【图形学】课程大作业

mac2025-10-12  7

【图形学】课程作业绘制机器人骨架

绘制球体(顶点数组和索引数组)绘制机器人各个部位绘制光源交互功能实现添加光照

绘制球体(顶点数组和索引数组)

GLint statcky = 60; // 横向向切成多少片 GLint stlicex = 60; // 纵向切多少片 const float PI = 3.1415926; std::vector<float> drawglobeVBO() { std::vector<float> c; GLfloat R = 0.5f; // 半径 GLfloat angleHy = (GLfloat)(2 * PI) / statcky; // 横向每份的角度 算出弧度值 GLfloat angleZx = (GLfloat)(2 * PI) / stlicex; // 纵向每份的角度 算出弧度值 GLfloat NumAngleHy = 0; // 当前横向角度 GLfloat NumAngleZx = 0; // 当前纵向角度 GLfloat x = 0; GLfloat y = 0; GLfloat z = 0; for (float j = 0; j < statcky; j++) { for (float i = 0; i < stlicex; i++) { NumAngleHy = angleHy * i; NumAngleZx = angleZx * j; // 起点都是轴指向的方向。根据右手定则决定转向,只要转向相同,那么两个就合适 GLfloat x = R * cos(NumAngleHy) * cos(NumAngleZx); // 记得转化精度 yaw&&pitch GLfloat y = R * sin(NumAngleHy); GLfloat z = R * cos(NumAngleHy) * sin(NumAngleZx); c.push_back(x); c.push_back(y); c.push_back(z); } } return c; } std::vector<int> drawglobeEBO() { std::vector<int> ebo; std::vector<float> vbo = drawglobeVBO(); int only = vbo.size(); int num = (int)((only / (3 * statcky)) * 2); for (int x = 0; x < stlicex / 2;) { for (int y = 0; y < statcky; y++) { ebo.push_back(y + x * stlicex); ebo.push_back(y + x * stlicex + 1); ebo.push_back(y + x * stlicex + stlicex); ebo.push_back(y + x * stlicex + stlicex + 1); ebo.push_back(y + x * stlicex + stlicex); ebo.push_back(y + x * stlicex + 1); } x = x + 1; } return ebo; }

参考链接:使用OpenGL动态管线和C++完成一个球体

声明球体的顶点数组和索引数组。

std::vector<float> vertices = drawglobeVBO(); std::vector<int> indices = drawglobeEBO();

通过顶点缓冲对象(Vertex Buffer Objects, VBO)管理内存,使用glGenBuffers函数和一个缓冲ID生成一个VBO对象并使用glBindBuffer函数把新创建的缓冲绑定到GL_ARRAY_BUFFER目标上:

unsigned int VBO; glGenBuffers(1, &VBO); glBindBuffer(GL_ARRAY_BUFFER, VBO);

调用glBufferData函数,它会把之前定义的顶点数据复制到缓冲的内存中:

glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(float), &vertices[0], GL_STATIC_DRAW);

创建一个VAO和创建一个VBO很类似,这里我声明了两个VAO对象,一个是机器人的一个是光源的:

unsigned int robotVAO, lampVAO; glGenVertexArrays(1, &robotVAO); glGenVertexArrays(1, &lampVAO);

使用glBindVertexArray绑定VAO。绑定后,绑定和配置对应的VBO和属性指针,之后解绑VAO供之后使用。绘制机器人的时候,在绘制物体前简单地把VAO绑定到希望使用的设定上就行了。

// 1. 绑定VAO glBindVertexArray(robotVAO); // 2. 把顶点数组复制到缓冲中供OpenGL使用 glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 3. 设置顶点属性指针 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); glEnableVertexAttribArray(0); [...] // ..:: 绘制代码(渲染循环中) :: .. // 4. 绘制物体 glUseProgram(shaderProgram); glBindVertexArray(robotVAO); someOpenGLFunctionThatDrawsOurTriangle();

创建索引缓冲对象,与VBO类似,我们先绑定EBO然后用glBufferData把索引复制到缓冲里。同样,和VBO类似,我们会把这些函数调用放在绑定和解绑函数调用之间,只不过这次我们把缓冲的类型定义为GL_ELEMENT_ARRAY_BUFFER。

unsigned int EBO; glGenBuffers(1, &EBO); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(int), &indices[0], GL_STATIC_DRAW); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); glEnableVertexAttribArray(0);

绘制机器人各个部位

首先绑定robotVAO,建立model(变量名为test_head),VAO对象就是一开始绘制的球体,通过缩放(scale)把一个球体拉伸为一个椭球体,旋转变换为交互模块所用,实现通过按住按键“Z”,实现机器人的转头操作。

glBindVertexArray(robotVAO); glm::mat4 test_head = glm::mat4(1.0f); test_head = glm::scale(test_head, glm::vec3(0.8f, 1.0f, 1.2f)); test_head = glm::rotate(test_head, hangle, glm::vec3(0.0f, 1.0f, 0.0f)); glm::mat4 MVP = projection * view * test_head; lightShader.use();

通过球体的平移(transport)、缩放(scale),绘制机器人的肩膀,并将透视、投影、模型矩阵的值传给着色器。

glm::mat4 test_shoulder = glm::mat4(1.0f); test_shoulder = glm::translate(test_shoulder, glm::vec3(0.0f, -0.5f, 0.0f)); test_shoulder = glm::scale(test_shoulder, glm::vec3(3.0f, 0.3f, 0.8f)); MVP = projection * view * test_shoulder; lightShader.setMat4("MVP", MVP); glBindVertexArray(robotVAO);

同理可得机器人的其他部位。

绘制光源

先定义光源位置,设定光源在机器人头部正上前方(0,1,2):

glm::vec3 lightPos(0.0f, 1.0f, 2.0f);

激活lampShader着色器程序,绘制lamp。

lampShader.use(); glm::mat4 lamp = glm::mat4(1.0f); lamp = glm::translate(lamp, lightPos); lamp = glm::scale(lamp, glm::vec3(0.2f)); MVP = projection * view * lamp; lightShader.setMat4("MVP", MVP); glBindVertexArray(lampVAO); glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, (void*)0);

交互功能实现

首先是键盘操作,通过W、S、A、D来向前、后、左、右变换摄像头的位置,以此来实现机器人的放大、缩小、左移和右移;通过X来实现机器人走路的操作,通过Z来实现机器人头部的旋转操作。

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_X) == GLFW_PRESS) { aangle = sin(glfwGetTime() * 2) / 3; langle = sin(glfwGetTime() * 2) / 2; //std::cout << "x!!" << std::endl; } if (glfwGetKey(window, GLFW_KEY_Z) == GLFW_PRESS) { hangle = glfwGetTime(); } }

其次是鼠标操作,在调用glfwSetInputMode函数之后,无论我们怎么去移动鼠标,光标都不会显示了,它也不会离开窗口。

glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);

为了计算俯仰角和偏航角,我们需要让GLFW监听鼠标移动事件。(和键盘输入相似)我们会用一个回调函数来完成,函数的原型如下:

void mouse_callback(GLFWwindow* window, double xpos, double ypos){ if (firstmouse) { lastX = xpos; lastY = ypos; firstmouse = false; } float xoffset = xpos - lastX; float yoffset = lastY - ypos; ; lastX = xpos; lastY = ypos; camera.ProcessMouseMovement(xoffset, yoffset); }

这里的xpos和ypos代表当前鼠标的位置。当我们用GLFW注册了回调函数之后,鼠标一移动mouse_callback函数就会被调用:

glfwSetCursorPosCallback(window, mouse_callback);

使用鼠标的滚轮来放大。与鼠标移动、键盘输入一样,我们需要一个鼠标滚轮的回调函数:

void scroll_callback(GLFWwindow* window, double xoffset, double yoffset) { camera.ProcessMouseScroll(yoffset); }

添加光照

环境光照:用光的颜色乘以一个很小的常量环境因子,再乘以物体的颜色,然后将最终结果作为片段的颜色。 漫反射:光的方向向量是光源位置向量与片段位置向量之间的向量差,并且对向量进行标准化。对norm和lightDir向量进行点乘,计算光源对当前片段实际的漫发射影响。结果值再乘以光的颜色,得到漫反射分量。两个向量之间的角度越大,漫反射分量就会越小。如果两个向量之间的角度大于90度,点乘的结果就会变成负数,这样会导致漫反射分量变为负数。为此,使用max函数返回两个参数之间较大的参数,从而保证漫反射分量不会变成负数。 镜面反射:为简单地使用摄像机对象的位置坐标代替,因此把相应的摄像机位置坐标传给片段着色器。定义一个镜面强度(Specular Intensity)变量,给镜面高光一个中等亮度颜色,让它不要产生过度的影响。 把镜面光加到环境光分量和漫反射分量里,再用结果乘以物体的颜色,得到最终的光照结果。 着色器语言:

#version 330 core out vec4 FragColor; in vec3 Normal; in vec3 FragPos; uniform vec3 lightPos; uniform vec3 viewPos; uniform vec3 lightColor; uniform vec3 objectColor; void main() { // ambient float ambientStrength = 0.1; vec3 ambient = ambientStrength * lightColor; // diffuse vec3 norm = normalize(Normal); vec3 lightDir = normalize(lightPos - FragPos); float diff = max(dot(norm, lightDir), 0.0); vec3 diffuse = diff * lightColor; // specular float specularStrength = 0.5; vec3 viewDir = normalize(viewPos - FragPos); vec3 reflectDir = reflect(-lightDir, norm); float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32); vec3 specular = specularStrength * spec * lightColor; vec3 result = (ambient + diffuse + specular) * objectColor; FragColor = vec4(result, 1.0); }
最新回复(0)