[Hello Triangle](https://learnopengl.com/Getting-started/Hello-Triangle) ## Basic concept ==Primitive== 指定坐标和颜色的渲染类型,包括`GL_POINTS, GL_TRIANGLES and GL_LINE_STRIP` ==fragments== 构成单个像素所需的数据 `OpenGL`发生在三维空间,但是我们的屏幕是二维的,坐标系的转换依赖`graphics pipeline`,各个阶段如下所示: ![pipeline](https://i-blog.csdnimg.cn/blog_migrate/4a67084537952f98f2fb8f4901d04e01.png =500x) 每一步的输出都将作为下一步的输入。 1. `Vertex Shader`:对输入的顶点数据进行变换 2. `Primitive Assembly`:将变换过的顶点数据装配成`Primitive`指定的形状 3. `Geometry Shader`:可以产生新的顶点数据构造出其它`Primitive` 4. `Rasterization Stage`:将`Primitive`映射成屏幕上的像素,生成`fragments`,并且会丢弃掉屏幕外的`fragments` 5. `Fragment Shader`:产生像素最终绘制的颜色,包括光照、阴影等因素 6. `Blending`:进行深度测试,并且会考虑透明度 ### Vertex Buffer Objects(VBO) 输入的顶点坐标要求用`Normalized Device Coordinates (NDC)`,即限制范围为`[-1.0, 1.0]`: ![NDC](https://i-blog.csdnimg.cn/blog_migrate/dfaf2150130451ab6ee84de3a4cd5c0d.png =500x) `viewport transform`会利用`glViewport`提供的数据将`NDC`坐标转化为`screen-space coordinates`,最终作为`fragment shader`的输入。 通过`VBO`管理`GPU`内存上的顶点数据,优势在于可以一次性将数据送到显卡,毕竟从`CPU`传数据是比较耗时的。 ```cpp float vertices[] = { -0.5f, -0.5f, 0.0f, 0.5f, -0.5f, 0.0f, 0.0f, 0.5f, 0.0f }; unsigned int VBO; // buffer id glGenBuffers(1, &VBO); glBindBuffer(GL_ARRAY_BUFFER, VBO); // 绑定buffer到特定类型,如果buffer为0将重置当前buffer的边界为空状态? // 绑定数据到buffer区域,usage常用的有三种 // GL_STREAM_DRAW: the data is set only once and used by the GPU at most a few times. // GL_STATIC_DRAW: the data is set only once and used many times. // GL_DYNAMIC_DRAW: the data is changed a lot and used many times. glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); glBindBuffer(GL_ARRAY_BUFFER, 0); ``` ### Vertex shader 顶点着色器语法如下: ```cpp #version 330 core // 下面会用到location,被in标记的为输入量,称为vertex attributes // 至少有16个可用的输入,通过int nrAttributes; glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes); 可以获取可用数量 // 顶点属性最大允许的数据大小等于一个vec4。因为一个mat4本质上是4个vec4,我们需要为这个矩阵预留4个顶点属性。 layout (location = 0) in vec3 aPos; void main() { // 预定义的输出坐标 gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0); } ``` 编译顶点着色器的方式如下: ```c const char *vertexShaderSource = "#version 330 core\n" "layout (location = 0) in vec3 aPos;\n" "void main()\n" "{\n" " gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n" "}\0"; unsigned int vertexShader; // reference id vertexShader = glCreateShader(GL_VERTEX_SHADER); // 指定编译源码,第二个参数代表str的数量 glShaderSource(vertexShader, 1, &vertexShaderSource, NULL); // 可以通过glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);判断编译结果 glCompileShader(vertexShader); ``` ### Fragment shader 片段着色器定义了输出像素的颜色: ```c #version 330 core out vec4 FragColor; void main() { // 如果输出颜色失败,最终渲染出来会是黑色或者白色 FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f); } ``` 编译的方式与顶点着色器一致: ```c unsigned int fragmentShader; fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL); glCompileShader(fragmentShader); ``` 如果想让顶点着色器的输出作为片段着色器的一个输入,变量的类型和名称应该一致。 ### Shader program 需要将上述编译过的着色器`link`到`Shader program`对象上: ```c unsigned int shaderProgram; shaderProgram = glCreateProgram(); glAttachShader(shaderProgram, vertexShader); glAttachShader(shaderProgram, fragmentShader); glLinkProgram(shaderProgram); // 使用shader program glUseProgram(shaderProgram); glDeleteShader(vertexShader); glDeleteShader(fragmentShader); ``` ### Linking Vertex Attributes 我们需要指定解释顶点数据的方式,比如步长: ```cpp // 1、layout (location = 0) 定义的位置,即vertex attributes,对应顶点着色器的输入参数 // 2、vertex attribute,即 vec3 // 3、数据类型 // 4、是否需要将坐标 NDC 化 // 5、两个顶点数据间的步长 // 6、从 buffer 中开始读取的位置 // 读取的 buffer 就是之前 VBO 绑定的 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); // 使顶点着色器对应的输入可用,与上一步的参数1相对应 glEnableVertexAttribArray(0); ``` ### Vertex Array Object(VAO) `VAO`存储了`VBO`和`vertex attributes`的对应关系,优势在于只需绑定`VAO`就可以绘制顶点数据,并且方便切换。 ![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/e841ab0b337fcf9fe6aaefd36e528664.png =500x) 工作流如下: ```cpp // ..:: Initialization code (done once (unless your object frequently changes)) :: .. unsigned int VAO; glGenVertexArrays(1, &VAO); // 1. bind Vertex Array Object // 调用之后,随后的VBO、VEO、 第3步的内容都会绑定到 VAO glBindVertexArray(VAO); // 2. copy our vertices array in a buffer for OpenGL to use glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 3. then set our vertex attributes pointers glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); glEnableVertexAttribArray(0); [...] // ..:: Drawing code (in render loop) :: .. // 4. draw the object glUseProgram(shaderProgram); glBindVertexArray(VAO); // 1、想要绘制的类型 // 2、开始的顶点数据下标 // 3、绘制的顶点数量 glDrawArrays(GL_TRIANGLES, 0, 3); ``` ### Element Buffer Objects(EBO) 如果我们想绘制一个矩形,也就是两个三角形,六个顶点,但是会有两个顶点是重复的。那么可不可以预先定义好不重复的所有顶点,然后通过一个索引数组去指定我们想要绘制的顶点呢? 如此就引入了`EBO`的概念: ```cpp float vertices[] = { 0.5f, 0.5f, 0.0f, // top right 0.5f, -0.5f, 0.0f, // bottom right -0.5f, -0.5f, 0.0f, // bottom left -0.5f, 0.5f, 0.0f // top left }; unsigned int indices[] = { // note that we start from 0! 0, 1, 3, // first triangle 1, 2, 3 // second triangle }; unsigned int EBO; glGenBuffers(1, &EBO); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); // 可见EBO只是索引数组的buffer glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); // ..:: Initialization code :: .. // 1. bind Vertex Array Object glBindVertexArray(VAO); // 2. copy our vertices array in a vertex buffer for OpenGL to use glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 3. copy our index array in a element buffer for OpenGL to use glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); // 4. then set the vertex attributes pointers glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); glEnableVertexAttribArray(0); [...] // ..:: Drawing code (in render loop) :: .. glUseProgram(shaderProgram); glBindVertexArray(VAO); // 2、指定绘制的顶点数目 // 3、索引元素数据类型 // 4、EBO 的偏移 glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0) glBindVertexArray(0); ``` 现在`VAO`存储的信息又增加了`EBO`: ![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/42063c66a48b4b65f666a96bdcb10c4f.png =500x) ## Shader 除了通过上述的方式像着色器中传输数据,还可以用`uniform`关键词从`CPU`传输数据到`GPU`,这种方法是全局性的,在`shader program`的任何阶段都可以获取到,一旦设置会一直有效,知道重置或更新。如果声明但未赋值,会在编译阶段移除。 ```c #version 330 core out vec4 FragColor; uniform vec4 ourColor; // we set this variable in the OpenGL code. void main() { FragColor = ourColor; } ``` 可以通过下面的方法进行赋值: ```cpp float timeValue = glfwGetTime(); // 返回GLFW初始化以来的秒数 float greenValue = (sin(timeValue) / 2.0f) + 0.5f; int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor"); glUseProgram(shaderProgram); // 赋值前先 use // glUniform 有以下几种后缀: // f: the function expects a float as its value. // i: the function expects an int as its value. // ui: the function expects an unsigned int as its value. // 3f: the function expects 3 floats as its value. // fv: the function expects a float vector/array as its value. glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f); ``` 一般设置每一帧都需要变化的值可以这样做。 [**绘制顶点颜色不同的三角形**](https://learnopengl.com/code_viewer_gh.php?code=src/1.getting_started/3.2.shaders_interpolation/shaders_interpolation.cpp) ### 封装 [Shader Class](https://learnopengl.com/code_viewer_gh.php?code=includes/learnopengl/shader_s.h) 以后可以单独把`shader`写到一个文件里,使用的时候也很方便: ```cpp // 本地测试相对路径不太好使,可以先用绝对路径代替 Shader ourShader("path/to/shaders/shader.vs", "path/to/shaders/shader.fs"); [...] while(...) { ourShader.use(); ourShader.setFloat("someUniform", 1.0f); DrawStuff(); } ```