1 What is a graphics pipeline?
答:
Graphics pipeline 是图形渲染流水线。
一些资料将其翻译为“图形渲染管线”,个人不是太喜欢这样的翻译——把它理解为“流水线”之后便豁然开朗。Graphics pipeline 将图形渲染任务划分为若干个小任务,后一个任务的实施往往依赖于前一个任务的完成,各个任务彼此连接形成了一条渲染流水线。
另外通过查阅一些资料,发现 Graphics pipeline 并非有严格的划分,不同的资料会有不同的划分方法。一般来说将 Graphics pipeline 划分为以下四个阶段:应用程序阶段(Application)、几何处理阶段(Geometry Processing)、光栅化(Rasterization)和像素处理阶段(Pixel Processing)。应用程序阶段通常是在CPU端进行处理,包括碰撞检测、动画物理模拟以及视椎体剔除等任务,这个阶段会将数据送到渲染管线中;几何处理阶段主要执行顶点着色器、投影变换、裁剪和屏幕映射的功能;光栅化阶段将图元离散化片段,变成一个个含有信息的点;像素处理阶段包括像素着色和混合的功能。对比各类资料可以发现,虽然管线的划分粒度不一样,但是每个阶段的具体功能其实是差不多的,原理也是一样的,并没有太大的差异。
下图则是另一种划分方式,蓝色部分代表的是可以自定义的着色器的部分。
2 What are vertex, primitive and fragment?
答:
顶点(Vertex)是坐标的数据的集合,它可以包含任何有用的的数据,包括位置信息、颜色信息等。
图元(Primitive) 则告诉 OpenGL 这些顶点数据所表示的渲染类型——比如一个点、一个三角形、或是仅仅是一条线。
片段(Fragment)在各类资料中都与片段着色器(Fragment Shader)一起出现。它是供片段着色器使用的像素阵列,个人将其理解为一个没有渲染好的“帧”。它是这样生成的:几何着色器(Geometry Shader)-> 光栅化阶段(Rasterization Stage)–得到片段–> 片段着色器(Fragment Shader)->……
3 What are geometry and topology?
答:
几何(geometry)是指各个顶点所组成的几何形体,往往在几何处理阶段(Geometry Processor)起作用。在这个阶段,图元的完整数据(比如:所有的顶点数据)和相邻顶点的数据全部都提供给着色器(shader),使其必须眷顾更多的信息,包括除了顶点本身的其他的一些额外的更全面完整的信息。几何处理器还可以将输出的图形拓扑结构转换成在 draw call 中选择的另一种结构。
由上文可知,拓扑(topology)为各个顶点的组成方式——它们是以什么顺序连接在一起的、它们组成了一个怎样的图形等等。
4 What happens when the viewport is enlarged? Why?
答:
图像会等比例在变大的窗口里变大。视野不会变化。
因为改变 viewport 的大小,实际上只改变了视口中物体向屏幕像素之间的映射关系,并没有改变视口观察范围的大小。
5 What happens when the clipping window is enlarged? Why?
答:
视野里可被观察的物体会变多,视野范围变大,但屏幕窗口中的像素不会变化。
因为裁剪范围变大,可以理解为有更多范围内的物体能被虚拟摄像机拍摄下来。
6 How to represent a Solid object in computer graphics?
答:
将物体(object)的各个数据发送给 GPU,并告诉 GPU 如何解读这些数据,通过这些数据和解读方式就可以描绘(represent)这个物体。
在 OpenGL 中,使用 VBO、VAO、EBO 来实现。
顶点缓冲对象(Vertex Buffer Objects, VBO)在 GPU 内存(通常被称为显存)中储存大量顶点。使用这些缓冲对象,可以一次性的发送一大批数据到显卡上,而不是每个顶点发送一次。从 CPU 把数据发送到显卡相对较慢,所以只要可能要尝试尽量一次性发送尽可能多的数据。当数据发送至显卡的内存中后,顶点着色器几乎能立即访问顶点,这是个非常快的过程。
顶点数组对象(Vertex Array Object, VAO)记录着对应 VBO 的属性信息,比如如何解读 VBO 中的数据。它可以像顶点缓冲对象那样被绑定,任何随后的顶点属性调用都会储存在这个 VAO 中。这样的好处就是,当配置顶点属性指针时,只需要将那些调用执行一次,之后再绘制物体的时候只需要绑定相应的 VAO 就行了。这使在不同顶点数据和属性配置之间切换变得非常简单,只需要绑定不同的 VAO 就行了。刚刚设置的所有状态都将存储在 VAO 中。
索引缓冲对象(Element Buffer Object,EBO,也叫Index Buffer Object,IBO),是一个缓冲,它专门储存索引,OpenGL 调用这些顶点的索引来决定该绘制哪个顶点。为此可以避免在 VBO 中存入重复的顶点。
在后面绘制正方体的作业中,我使用了 EBO 来优化存储空间。
VBO、VAO、EBO 三者关系如下:
Programming
绘制一个方体
首先,展示绘制效果如下(做了一点小实验)(可能需要一些加载时间):
代码
我自己写了一个 ShaderCompiler 类,它能够完成编译链接 shader 程序的功能,只需在声明时指定 shader 文件路径或者使用 CompileShaderFile() 接口指定路径,然后通过 GetProgramId() 接口得到编译好的程序 ID。
ShaderCompiler 代码如下:
#include <glad/glad.h> #include <GLFW/glfw3.h> #include <stdio.h> #include <iostream> #include <fstream> #include <string> #include <sstream> class ShaderCompiler { private: std::string VertexShaderSource = ""; std::string FragmentShaderSource = ""; unsigned int program_id = 0; bool whether_compiled = false; private: /* 编译shader,并连接到program_id; 若此对象以前已经编译过,此函数会删除以前的program。 */ unsigned int CompileShader(unsigned int shaderType, const std::string& sourceCode) { if (whether_compiled) { glDeleteProgram(program_id); } whether_compiled = true; unsigned int id = glCreateShader(shaderType); const char* code = sourceCode.c_str(); glShaderSource(id, 1, &code, nullptr); glCompileShader(id); int result; glGetShaderiv(id, GL_COMPILE_STATUS, &result); if (result == GL_FALSE) { int length; glGetShaderiv(id, GL_INFO_LOG_LENGTH, &length); char* message = new(std::nothrow)char[length + 1]; glGetShaderInfoLog(id, length, &length, message); std::cout << "shader fail to compile:" << std::endl; std::cout << message << std::endl; glDeleteShader(id); delete[] message; return 0; } return id; } /* 从指定文件取到源码并存入VertexShaderSource、FragmentShaderSource */ void ParseShader(const std::string& filePath) { std::ifstream stream(filePath); if (stream.good() == 0) { std::cerr << "fail to open shader files" << std::endl; VertexShaderSource = ""; FragmentShaderSource = ""; return; } enum class ShaderType { NONE = -1, VERTEX = 0, FRAGMENT = 1 }; std::string line; std::stringstream ss[2]; ShaderType type = ShaderType::NONE; while (getline(stream, line)) { if (line.find("#shader") != std::string::npos) { if (line.find("vertex") != std::string::npos) { type = ShaderType::VERTEX; } else if (line.find("fragment") != std::string::npos) { type = ShaderType::FRAGMENT; } } else { if (type == ShaderType::NONE) { continue; } ss[int(type)] << line << std::endl; } } VertexShaderSource = ss[int(ShaderType::VERTEX)].str(); FragmentShaderSource = ss[int(ShaderType::FRAGMENT)].str(); return; } /* 调用CompileShader()生成程序。 需事先知道源码。 */ unsigned int CreateShader(const std::string& vertexShaderCode, const std::string& fragmentShaderCode) { unsigned int program = glCreateProgram(); unsigned int vs = CompileShader(GL_VERTEX_SHADER, vertexShaderCode); unsigned int fs = CompileShader(GL_FRAGMENT_SHADER, fragmentShaderCode); glAttachShader(program, vs); glAttachShader(program, fs); glLinkProgram(program); glValidateProgram(program); glDeleteShader(vs); glDeleteShader(fs); return program; } public: ShaderCompiler(const std::string filepath) { ParseShader(filepath); program_id = CreateShader(VertexShaderSource, FragmentShaderSource); } ShaderCompiler() { ; } /* 提供shader文件路径,直接返回编译好的program_id */ unsigned int CompileShaderFile(const std::string filepath) { ParseShader(filepath); program_id = CreateShader(VertexShaderSource, FragmentShaderSource); return program_id; } /* 不编译连接,直接返回内部的program_id */ unsigned int GetProgramId() { return program_id; } };
绘制正方体所用的 shader 代码如下:
#shader vertex #version 330 core layout (location = 0) in vec3 aPos; layout(location = 1) in vec3 aColor; out vec4 oColor; uniform mat4 model; uniform mat4 view; uniform mat4 projection; void main() { gl_Position = projection * view * model * vec4(aPos.x, aPos.y, aPos.z, 1.0f); oColor = vec4(aColor, 1.0f); } ///////////////////////////////// #shader fragment #version 330 core out vec4 fragColor; in vec4 oColor; void main() { fragColor = oColor; }
main() 程序代码如下:
#include <glad/glad.h> #include <GLFW/glfw3.h> #include <stdio.h> #include <iostream> #include <fstream> #include <string> #include <sstream> //glm #include <glm/glm.hpp> #include <glm/gtc/matrix_transform.hpp> #include <glm/gtc/type_ptr.hpp> #include "ShaderCompiler.hpp" void framebuffer_size_callback(GLFWwindow* window, int width, int height); void processInput(GLFWwindow* window); // settings const unsigned int SCR_WIDTH = 1100; const unsigned int SCR_HEIGHT = 1100; int main() { glfwInit(); glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "HELLO WORLD", nullptr, nullptr); if (window == nullptr) { std::cout << "fail to create GLFW window" << std::endl; glfwTerminate(); return -1; } glfwMakeContextCurrent(window);//将窗口上下文 设置为 当前线程的主上下文 if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { std::cout << "fail to initialize GLAD" << std::endl; return -1; } glViewport(0, 0, SCR_WIDTH, SCR_HEIGHT);//glViewport函数前两个参数控制窗口左下角的位置。第三个和第四个参数控制渲染窗口的宽度和高度(像素) glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);//注册函数,告诉GLFW我们希望每当窗口调整大小的时候调用这个函数 glEnable(GL_DEPTH_TEST); const int vertex_array_num = 48; float vertex_position_color[vertex_array_num] = { //x,y,z,r,g,b 0.5f, 0.5f, 0.5f, 0.9f, 0.3f, 0.3f,//0 0.5f, 0.5f, -0.5f, 0.1f, 0.9f, 0.1f,//1 -0.5f, 0.5f, -0.5f, 0.1f, 0.1f, 0.9f,//2 -0.5f, 0.5f, 0.5f, 0.9f, 0.9f, 0.9f,//3 0.5f, -0.5f, 0.5f, 0.3f, 0.3f, 0.9f,//4 0.5f, -0.5f, -0.5f, 0.9f, 0.9f, 0.9f,//5 -0.5f, -0.5f, -0.5f, 0.9f, 0.3f, 0.3f,//6 -0.5f, -0.5f, 0.5f, 0.3f, 0.9f, 0.3f//7 }; const int ebo_num = 36; int ebo_idx[ebo_num] = { 1, 5, 6, 1, 6, 2, 1, 0, 2, 0, 3, 2, 0, 3, 4, 3, 4, 7, 4, 5, 6, 4, 6, 7, 0, 4, 5, 0, 1, 5, 2, 3, 6, 3, 6, 7 }; unsigned int vertex_buffer_id, VAO_id, EBO_id; glGenVertexArrays(1, &VAO_id); glGenBuffers(1, &vertex_buffer_id); glGenBuffers(1, &EBO_id); glBindVertexArray(VAO_id); glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer_id); glBufferData(GL_ARRAY_BUFFER, vertex_array_num * sizeof(float), vertex_position_color, GL_STATIC_DRAW); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO_id); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(ebo_idx), ebo_idx, GL_STATIC_DRAW); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(float) * 6, (void*)0); glEnableVertexAttribArray(0); glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(float) * 6, (void*)(sizeof(float) * 3)); glEnableVertexAttribArray(1); glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0); ShaderCompiler sc("./shader.shader"); unsigned int program_id = sc.GetProgramId(); while (!glfwWindowShouldClose(window)) { processInput(window); glClearColor(0.3f, 0.7f, 1.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glUseProgram(program_id); glBindVertexArray(VAO_id); glm::mat4 model = glm::mat4(1.0f); glm::mat4 view = glm::mat4(1.0f); glm::mat4 projection = glm::mat4(1.0f); model = glm::rotate(model, (float)glfwGetTime(), glm::vec3(1.0f, 1.0f, 1.0f)); view = glm::translate(view, glm::vec3(0.0f, 0.0f, -5.0f)); projection = glm::perspective(glm::radians(45.0f), (float)SCR_WIDTH / SCR_HEIGHT, 0.1f, 100.0f); unsigned int modelLoc = glGetUniformLocation(program_id, "model"); unsigned int viewLoc = glGetUniformLocation(program_id, "view"); unsigned int projectionLoc = glGetUniformLocation(program_id, "projection"); glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model)); glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view)); glUniformMatrix4fv(projectionLoc, 1, GL_FALSE, glm::value_ptr(projection)); glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_INT, (void*)0); glBindVertexArray(0); glfwSwapBuffers(window); glfwPollEvents(); } glDeleteProgram(program_id); glfwTerminate(); return 0; } void framebuffer_size_callback(GLFWwindow* window, int width, int height) { glViewport(0, 0, width, height); } void processInput(GLFWwindow* window) { if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) { glfwSetWindowShouldClose(window, true); } }
以上。
完成本次作业。
Eol 提供 Web 技术支持
特别鸣谢:Eol
Leave a Reply