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