OpenGL-02-创建三角形,矩形
OpenGL-02-创建三角形/矩形
逐步解析
原文:https://learnopengl-cn.github.io/01%20Getting%20started/04%20Hello%20Triangle/#_3
本篇,主要对此文做个人总结以及思路简化
这里首先要讲解一些基本知识
对于OpenGL,我们需要至少设置一个顶点着色器和一个片段着色器。通过他们我们才能绘制三角形
此外,OpenGL着色器使用GLSL语言
这是一段十分基础的GLSL顶点着色器源代码
#version 330 core
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";
创建顶点着色器
通过glCreateShader(GL_VERTEX_SHADER);
创建顶点着色器对象,输入参数为着色器类型,这里是顶点着色器
并通过 glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
语句将之前的GLSL源代码附加到着色器对象上,在编译着色器即可。
// build and compile our shader program 创建和编码 着色器程序
// ------------------------------------
// vertex shader 顶点着色器
//创建一个着色器对象
//通过glCreateShader函数进行着色器创建,输入参数为着色器类型
unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);
//把这个着色器源码附加到着色器对象上,然后编译它
//参数:把要编译的着色器对象作为第一个参数。第二参数指定了传递的源码字符串数量
//这里只有一个。第三个参数是顶点着色器真正的源码,第四个参数我们先设置为NULL。
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
//编译着色器
glCompileShader(vertexShader);
// check for shader compile errors
//检查着色器编译是否成功
int success;
char infoLog[512];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
}
同理,片段着色器的创建也是一样的
//片段着色器源代码
const char *fragmentShaderSource = "#version 330 core\n"
"out vec4 FragColor;\n"
"void main()\n"
"{\n"
" FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n"
"}\n\0";
// fragment shader 片段着色器
//通过glCreateShader函数进行着色器创建
unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
//将片段着色器附加到着色器对象上,然后编译它
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
//编译着色器
glCompileShader(fragmentShader);
// check for shader compile errors
//检查着色器是否编译成功
glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;
}
创建了两个着色器对象并编译他们后,我们还需要将他们链接到一个用来渲染的着色器程序上。
着色器程序对象(Shader Program Object)
是多个着色器合并之后并最终链接完成的版本。如果要使用刚才编译的着色器我们必须把它们链接(Link)为一个着色器程序对象,然后在渲染对象的时候激活这个着色器程序。已激活着色器程序的着色器将在我们发送渲染调用的时候被使用。
通过unsigned int shaderProgram = glCreateProgram();
创建着色器程序
并通过glAttachShader
函数将之前的两个着色器附加到这个着色器程序上
再通过glLinkProgram
函数链接这些着色器,即可。
最后再将两个着色器对象删除,因为已经链接到了着色器程序上,我们就不在需要他们。
// link shaders 链接着色器
//创建一个着色器程序
//着色器程序对象(Shader Program Object)是多个着色器合并之后并最终链接完成的版本。
//如果要使用刚才编译的着色器我们必须把它们链接(Link)为一个着色器程序对象,
//然后在渲染对象的时候激活这个着色器程序。已激活着色器程序的着色器将在我们发送渲染调用的时候被使用。
unsigned int shaderProgram = glCreateProgram();
//将顶点着色器附加到着色器程序上
glAttachShader(shaderProgram, vertexShader);
//将片段着色器附加到着色器程序上
glAttachShader(shaderProgram, fragmentShader);
//使用glLinkProgram链接这些着色器
glLinkProgram(shaderProgram);
// check for linking errors
//检查 链接着色器是否成功
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if (!success) {
glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
}
// 将着色器连接到程序对象之后,删除着色器对象
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
至此,我们已经完成了着色器的创建,最后只通过以下代码需要激活着色器程序即可
glUseProgram(shaderProgram);
到这里,虽然已经有了能够渲染的着色器程序。但是我们还没有东西给他去渲染
接下来我将解释,如果去渲染一个三角形或者矩形,先说三角形
绘制三角形
首先我们要指定三角形的顶点
float vertices[] = {//三角形
-0.5f, -0.5f, 0.0f, // left
0.5f, -0.5f, 0.0f, // right
0.0f, 0.5f, 0.0f // top
};
由于我们要绘制的是2D三角形,因此将z轴都设置为0。
有了顶点后,接下来我需要先介绍两个极其重要的概念
VBO(顶点缓冲对象)
顶点缓冲对象VBO是在显卡存储空间中开辟出的一块内存缓存区,用于存储顶点的各类属性信息,如顶点坐标,顶点法向量,顶点颜色数据等。在渲染时,可以直接从VBO中取出顶点的各类属性数据,由于VBO在显存而不是在内存中,不需要从CPU传输数据,处理效率更高。
所以可以理解为VBO就是显存中的一个存储区域,可以保持大量的顶点属性信息。并且可以开辟很多个VBO,每个VBO在OpenGL中有它的唯一标识ID,这个ID对应着具体的VBO的显存地址,通过这个ID可以对特定的VBO内的数据进行存取操作。
简单来说,VBO就是用来存储上面我们指定的顶点坐标
首先创建VBO
unsigned int VBO;
然后使用glGenBuffers
函数和一个缓冲ID
生成一个VBO对象:
创建的VBO可用来保存不同类型的顶点数据,创建之后需要通过分配的ID绑定(bind)制定的VBO,
glGenBuffers(1, &VBO);
对于同一类型的顶点数据一次只能绑定一个VBO。绑定操作通过glBindBuffer来实现,第一个参数指定绑定的数据类型,可以是GL_ARRAY_BUFFER, GL_ELEMENT_ARRAY_BUFFER, GL_PIXEL_PACK_BUFFER或者GL_PIXEL_UNPACK_BUFFER中的一个。
简单来说,下面这行代码就是指定VBO要绑定的缓存类型,上面这些就是缓存类型
glBindBuffer(GL_ARRAY_BUFFER, VBO);
接下来调用glBufferData
把用户定义的数据传输到当前绑定的显存缓冲区中。
glBufferData
是一个专门用来把用户定义的数据复制到当前绑定缓冲的函数。它的第一个参数是目标缓冲的类型:顶点缓冲对象当前绑定到GL_ARRAY_BUFFER目标上。第二个参数指定传输数据的大小(以字节为单位);用一个简单的sizeof
计算出顶点数据大小就行。第三个参数是我们希望发送的实际数据。
第四个参数指定了我们希望显卡如何管理给定的数据。它有三种形式:
- GL_STATIC_DRAW :数据不会或几乎不会改变。
- GL_DYNAMIC_DRAW:数据会被改变很多。
- GL_STREAM_DRAW :数据每次绘制时都会改变。
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
顶点数据传入GPU之后,还需要通知OpenGL如何解释这些顶点数据,这个工作由函数glVertexAttribPointer
完成:
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
- 第一个参数指定顶点属性位置值,与顶点着色器中
layout(location=0)
对应。 - 第二个参数指定顶点属性大小,顶点属性是一个
vec3
,它由3个值组成,所以大小是3。 - 第三个参数指定数据类型。
- 第四个参数定义是否希望数据被标准化。
- 第五个参数是步长(Stride),指定在连续的顶点属性之间的间隔,这里每个数据都有三个
float
,每组数据之前相差3个float
的长度 - 第六个参数表示我们的位置数据在缓冲区起始位置的偏移量。
顶点属性glVertexAttribPointer
默认是关闭的,使用时要以顶点属性位置值为参数调用glEnableVertexAttribArray
开启
glEnableVertexAttribArray(0);
不过这样仍待不够,这里还需要引入第二个重要概念
VAO(顶点数组)
顶点数组对象(Vertex Array Object, VAO)可以像顶点缓冲对象那样被绑定,任何随后的顶点属性调用都会储存在这个VAO中。这样的好处就是,当配置顶点属性指针时,你只需要将那些调用执行一次,之后再绘制物体的时候只需要绑定相应的VAO就行了。这使在不同顶点数据和属性配置之间切换变得非常简单,只需要绑定不同的VAO就行了。刚刚设置的所有状态都将存储在VAO中
OpenGL的核心模式要求我们使用VAO,所以它知道该如何处理我们的顶点输入。如果我们绑定VAO失败,OpenGL会拒绝绘制任何东西。
VAO是一个保存了所有顶点数据属性的状态结合,它存储了顶点数据的格式以及顶点数据所需的VBO对象的引用。
VAO本身并没有存储顶点的相关属性数据,这些信息是存储在VBO中的,VAO相当于是对很多个VBO的引用,把一些VBO组合在一起作为一个对象统一管理。
接下来我们创建VAO对象
unsigned int VAO;
创建方式和创建VBO很相似,都是通过缓存ID进行创建
glGenVertexArrays(1, &VAO);
要想使用VAO,要做的只是使用glBindVertexArray
绑定VAO。从绑定之后起,我们应该绑定和配置对应的VBO和属性指针,之后解绑VAO供之后使用。当我们打算绘制一个物体的时候,我们只要在绘制物体前简单地把VAO绑定到希望使用的设定上就行了。
glBindVertexArray(VAO);
至此,对于VAO与VBO的创建就完成了
他们的完整代码如下,注释里,理解不了的后面会讲,主要看代码
//创建VBO(顶点缓冲对象),VAO(顶点数组对象),EBO(索引缓冲对象)
unsigned int VBO;
unsigned int VAO;
// unsigned int EBO;
//通过缓冲ID生成一个VAP对象
glGenVertexArrays(1, &VAO);
//使用glGenBuffers函数和一个缓冲ID生成一个VBO对象
glGenBuffers(1, &VBO);
//同样,通过glGenBuffers函数和缓冲ID生成EBO对象
// glGenBuffers(1, &EBO);
// bind the Vertex Array Object first, then bind and set vertex buffer(s), and then configure vertex attributes(s).
//使用glBindVertexArray绑定VAO
glBindVertexArray(VAO);
//顶点缓冲对象的缓冲类型是:GL_ARRAY_BUFFER
//OpenGL允许我们同时绑定多个缓冲,只要他们是不同的缓冲类型
//我们使用glBindBuffer将新创建的缓冲,绑定到GL_ARRAY_BUFFER上
//从这一刻起,我们使用的任何(在GL_ARRAY_BUFFER目标上的)缓冲调用都会用来配置当前绑定的缓冲(VBO)。
glBindBuffer(GL_ARRAY_BUFFER, VBO);
//我们可以调用glBufferData函数,它会把之前定义的顶点数据复制到缓冲的内存中:
//第四个参数指定我们希望显卡如何管理给定的数据
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
//绑定索引缓冲对象,缓冲对象类型为:GL_ELEMENT_ARRAY_BUFFER
// glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
//通过glBufferData将索引复制到缓冲里
// glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
//通过glVertexAttribPointer告诉OpenGL如何解析顶点数据
//第一个参数为我们要配置的顶点属性,
// 还记得我们在顶点着色器中使用layout(location = 0)定义了position顶点属性的位置值(Location)吗?
// 它可以把顶点属性的位置值设置为0。
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
//我们现在应该使用glEnableVertexAttribArray,以顶点属性位置值作为参数,启用顶点属性;
//顶点属性默认是禁用的。自此,所有东西都已经设置好了:我们使用一个顶点缓冲对象将顶点数据初始化至缓冲中,
// 建立了一个顶点和一个片段着色器,
// 并告诉了OpenGL如何把顶点数据链接到顶点着色器的顶点属性上。在OpenGL中绘制一个物体,
//启用顶点属性
glEnableVertexAttribArray(0);
OpenGL中所有的图形都是通过分解成三角形的方式进行绘制,glDrawArrays函数负责把模型绘制出来,它使用当前激活的着色器,当前VAO对象中的VBO顶点数据和属性配置来绘制出来基本图形。
glDrawArrays (GLenum mode, GLint first, GLsizei count)
-
第一个参数表示绘制的类型,有三种取值:
- 1.GL_TRIANGLES:每三个顶之间绘制三角形,之间不连接;
- 2.GL_TRIANGLE_FAN:以V0V1V2,V0V2V3,V0V3V4,……的形式绘制三角形;
- 3.GL_TRIANGLE_STRIP:顺序在每三个顶点之间均绘制三角形。这个方法可以保证从相同的方向上所有三角形均被绘制。以V0V1V2,V1V2V3,V2V3V4……的形式绘制三角形;
-
第二个参数定义从缓存中的哪一位开始绘制,一般定义为0;
-
第三个参数定义绘制的顶点数量;
在渲染指令中加上这行代码:
glDrawArrays(GL_TRIANGLES, 0, 3);
这样就能完成三角形的绘制了
三角形完整注释代码
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>
void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow *window);
// settings
//屏幕大小
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;
#pragma region 着色器源码
//顶点着色器源代码
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";
//片段着色器源代码
const char *fragmentShaderSource = "#version 330 core\n"
"out vec4 FragColor;\n"
"void main()\n"
"{\n"
" FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n"
"}\n\0";
#pragma endregion
int main()
{
#pragma region 窗口初始化与配置
// glfw: initialize and configure 初始化与配置
// ------------------------------
//glfw初始化
glfwInit();
//glfw配置
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
#ifdef __APPLE__
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif
#pragma endregion
// glfw window creation 创建窗口
// --------------------
GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
if (window == NULL)
{
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
//回调函数
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
// glad: load all OpenGL function pointers 通过glad管理所有的OpenGL函数指针
// ---------------------------------------
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
#pragma region shader
// build and compile our shader program 创建和编码 着色器程序
// ------------------------------------
// vertex shader 顶点着色器
//创建一个着色器对象
//通过glCreateShader函数进行着色器创建,输入参数为着色器类型
unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);
//把这个着色器源码附加到着色器对象上,然后编译它
//参数:把要编译的着色器对象作为第一个参数。第二参数指定了传递的源码字符串数量
//这里只有一个。第三个参数是顶点着色器真正的源码,第四个参数我们先设置为NULL。
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
//编译着色器
glCompileShader(vertexShader);
// check for shader compile errors
//检查着色器编译是否成功
int success;
char infoLog[512];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
}
// fragment shader 片段着色器
//通过glCreateShader函数进行着色器创建
unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
//将片段着色器附加到着色器对象上,然后编译它
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
//编译着色器
glCompileShader(fragmentShader);
// check for shader compile errors
//检查着色器是否编译成功
glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;
}
// link shaders 链接着色器
//创建一个着色器程序
//着色器程序对象(Shader Program Object)是多个着色器合并之后并最终链接完成的版本。
//如果要使用刚才编译的着色器我们必须把它们链接(Link)为一个着色器程序对象,
//然后在渲染对象的时候激活这个着色器程序。已激活着色器程序的着色器将在我们发送渲染调用的时候被使用。
unsigned int shaderProgram = glCreateProgram();
//将顶点着色器附加到着色器程序上
glAttachShader(shaderProgram, vertexShader);
//将片段着色器附加到着色器程序上
glAttachShader(shaderProgram, fragmentShader);
//使用glLinkProgram链接这些着色器
glLinkProgram(shaderProgram);
// check for linking errors
//检查 链接着色器是否成功
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if (!success) {
glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
}
// 将着色器连接到程序对象之后,删除着色器对象
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
glUseProgram(shaderProgram);
#pragma endregion
// set up vertex data (and buffer(s)) and configure vertex attributes
//顶点输入
// ------------------------------------------------------------------
float vertices[] = {//三角形
-0.5f, -0.5f, 0.0f, // left
0.5f, -0.5f, 0.0f, // right
0.0f, 0.5f, 0.0f // top
};
//为了绘制矩形,我们可以通过绘制两个三角形,来绘制举行
//但是会造成产生两个顶点的额外开销,当这个数量变大后,会造成大量的资源浪费
// float vertices[] = {
// // 第一个三角形
// 0.5f, 0.5f, 0.0f, // 右上角
// 0.5f, -0.5f, 0.0f, // 右下角
// -0.5f, 0.5f, 0.0f, // 左上角
// // 第二个三角形
// 0.5f, -0.5f, 0.0f, // 右下角
// -0.5f, -0.5f, 0.0f, // 左下角
// -0.5f, 0.5f, 0.0f // 左上角
// };'
//因此,只储存不同的顶点,并设定绘制这些顶点的顺序。这样子我们只要储存4个顶点就能绘制矩形了,之后只要指定绘制的顺序就行了。
// float vertices[] = {
// 0.5f, 0.5f, 0.0f, // 右上角
// 0.5f, -0.5f, 0.0f, // 右下角
// -0.5f, -0.5f, 0.0f, // 左下角
// -0.5f, 0.5f, 0.0f // 左上角
// };
//
// unsigned int indices[] = { // 注意索引从0开始!
// 0, 1, 3, // 第一个三角形
// 1, 2, 3 // 第二个三角形
// };
//创建VBO(顶点缓冲对象),VAO(顶点数组对象),EBO(索引缓冲对象)
unsigned int VBO;
unsigned int VAO;
// unsigned int EBO;
//通过缓冲ID生成一个VAP对象
glGenVertexArrays(1, &VAO);
//使用glGenBuffers函数和一个缓冲ID生成一个VBO对象
glGenBuffers(1, &VBO);
//同样,通过glGenBuffers函数和缓冲ID生成EBO对象
// glGenBuffers(1, &EBO);
// bind the Vertex Array Object first, then bind and set vertex buffer(s), and then configure vertex attributes(s).
//使用glBindVertexArray绑定VAO
glBindVertexArray(VAO);
//顶点缓冲对象的缓冲类型是:GL_ARRAY_BUFFER
//OpenGL允许我们同时绑定多个缓冲,只要他们是不同的缓冲类型
//我们使用glBindBuffer将新创建的缓冲,绑定到GL_ARRAY_BUFFER上
//从这一刻起,我们使用的任何(在GL_ARRAY_BUFFER目标上的)缓冲调用都会用来配置当前绑定的缓冲(VBO)。
glBindBuffer(GL_ARRAY_BUFFER, VBO);
//我们可以调用glBufferData函数,它会把之前定义的顶点数据复制到缓冲的内存中:
//第四个参数指定我们希望显卡如何管理给定的数据
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
//绑定索引缓冲对象,缓冲对象类型为:GL_ELEMENT_ARRAY_BUFFER
// glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
//通过glBufferData将索引复制到缓冲里
// glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
//通过glVertexAttribPointer告诉OpenGL如何解析顶点数据
//第一个参数为我们要配置的顶点属性,
// 还记得我们在顶点着色器中使用layout(location = 0)定义了position顶点属性的位置值(Location)吗?
// 它可以把顶点属性的位置值设置为0。
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
//我们现在应该使用glEnableVertexAttribArray,以顶点属性位置值作为参数,启用顶点属性;
//顶点属性默认是禁用的。自此,所有东西都已经设置好了:我们使用一个顶点缓冲对象将顶点数据初始化至缓冲中,
// 建立了一个顶点和一个片段着色器,
// 并告诉了OpenGL如何把顶点数据链接到顶点着色器的顶点属性上。在OpenGL中绘制一个物体,
//启用顶点属性
glEnableVertexAttribArray(0);
// note that this is allowed, the call to glVertexAttribPointer registered VBO as the vertex attribute's bound vertex buffer object so afterwards we can safely unbind
// glBindBuffer(GL_ARRAY_BUFFER, 0);
// You can unbind the VAO afterwards so other VAO calls won't accidentally modify this VAO, but this rarely happens. Modifying other
// VAOs requires a call to glBindVertexArray anyways so we generally don't unbind VAOs (nor VBOs) when it's not directly necessary.
// glBindVertexArray(0);
// uncomment this call to draw in wireframe polygons.
//glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
//OpenGL线框模式
// glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
// render loop
// -----------
while (!glfwWindowShouldClose(window))
{
// input
// -----
processInput(window);
// render
// ------
//清空并填充背景颜色
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// draw our first triangle
//激活着色器程序
//绑定顶点数组对象
// glBindVertexArray(VAO); // seeing as we only have a single VAO there's no need to bind it every time, but we'll do so to keep things a bit more organized
//它使用当前激活的着色器,之前定义的顶点属性配置,和VBO的顶点数据(通过VAO间接绑定)来绘制图元。
glDrawArrays(GL_TRIANGLES, 0, 3);
// glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
// glBindVertexArray(0); // no need to unbind it every time
// glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
// -------------------------------------------------------------------------------
glfwSwapBuffers(window);
glfwPollEvents();
}
// optional: de-allocate all resources once they've outlived their purpose:
// ------------------------------------------------------------------------
//
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
// glDeleteBuffers(1, &EBO);
glDeleteProgram(shaderProgram);
// glfw: terminate, clearing all previously allocated GLFW resources.
// ------------------------------------------------------------------
glfwTerminate();
return 0;
}
// process all input: query GLFW whether relevant keys are pressed/released this frame and react accordingly
// ---------------------------------------------------------------------------------------------------------
void processInput(GLFWwindow *window)
{
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
}
// glfw: whenever the window size changed (by OS or user resize) this callback function executes
// ---------------------------------------------------------------------------------------------
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
// make sure the viewport matches the new window dimensions; note that width and
// height will be significantly larger than specified on retina displays.
glViewport(0, 0, width, height);
}
/*
int main()
{
#pragma region windows
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);//MacOS系统
//参数分别为:长,宽,窗口标题,后面两个参数暂时忽略
GLFWwindow* window = glfwCreateWindow(800, 600, "MyFirstWindow", NULL, NULL);
if (window == NULL)
{
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
glViewport(0, 0, 800, 600);
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
// while(!glfwWindowShouldClose(window))
// {
// glfwSwapBuffers(window);
// glfwPollEvents();
// }
while (!glfwWindowShouldClose(window))
{
processInput(window);//通过esc关闭窗口
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glfwSwapBuffers(window);
glfwPollEvents();
}
glfwTerminate();
#pragma endregion
return 0;
}
*/
绘制矩形
绘制矩形,矩形其实就是两个三角形组成的,因此我们重新制定顶点
float vertices[] = {
// 第一个三角形
0.5f, 0.5f, 0.0f, // 右上角
0.5f, -0.5f, 0.0f, // 右下角
-0.5f, 0.5f, 0.0f, // 左上角
// 第二个三角形
0.5f, -0.5f, 0.0f, // 右下角
-0.5f, -0.5f, 0.0f, // 左下角
-0.5f, 0.5f, 0.0f // 左上角
};
这里有六个顶点,但是其实有两个是重复的,我们真正其实只需要4个顶点。
因此,这里我们需要引入一个重要概念
EBO(索引缓冲对象)
索引缓冲对象EBO相当于OpenGL中的顶点数组的概念,是为了解决同一个顶点多洗重复调用的问题,可以减少内存空间浪费,提高执行效率。当需要使用重复的顶点时,通过顶点的位置索引来调用顶点,而不是对重复的顶点信息重复记录,重复调用。
EBO中存储的内容就是顶点位置的索引indices,EBO跟VBO类似,也是在显存中的一块内存缓冲器,只不过EBO保存的是顶点的索引。
因此我们需要重新制定顶点,并指定索引
这次我们只指定四个顶点,实际上我们只需要四个顶点,但是指定六个索引,通过索引指向顶点的绘制就能节省内存空间的浪费。
float vertices[] = {
0.5f, 0.5f, 0.0f, // 右上角
0.5f, -0.5f, 0.0f, // 右下角
-0.5f, -0.5f, 0.0f, // 左下角
-0.5f, 0.5f, 0.0f // 左上角
};
unsigned int indices[] = { // 注意索引从0开始!
0, 1, 3, // 第一个三角形
1, 2, 3 // 第二个三角形
};
既然指定了索引,那么我们就需要有东西来存储它,这个东西就是EBO
接下来,我们创建EBO
unsigned int EBO;
同样,通过glGenBuffers
函数和缓冲ID生成EBO对象
glGenBuffers(1, &EBO);
绑定索引缓冲对象,缓冲对象类型为:GL_ELEMENT_ARRAY_BUFFER
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
通过glBufferData
将索引复制到缓冲里
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
此时,我们绘制的方式不是直接通过访问VBO顶点来绘制了,这时候我们是通过EBO绑定顶点索引的方式绘制模型,需要使用glDrawElements
而不是glDrawArrays
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
第一个参数指定了我们绘制的模式,这个和glDrawArrays的一样。
第二个参数是我们打算绘制顶点的个数,这里填6,也就是说我们一共需要绘制6个顶点。
第三个参数是索引的类型,这里是GL_UNSIGNED_INT。
最后一个参数里我们可以指定EBO中的偏移量(或者传递一个索引数组,但是这是当你不在使用索引缓冲对象的时候),但是我们会在这里填写0。
这里放上加了EBO创建的,包括VBO,VAO的完整注释代码
//创建VBO(顶点缓冲对象),VAO(顶点数组对象),EBO(索引缓冲对象)
unsigned int VBO;
unsigned int VAO;
unsigned int EBO;
//通过缓冲ID生成一个VAP对象
glGenVertexArrays(1, &VAO);
//使用glGenBuffers函数和一个缓冲ID生成一个VBO对象
glGenBuffers(1, &VBO);
//同样,通过glGenBuffers函数和缓冲ID生成EBO对象
glGenBuffers(1, &EBO);
// bind the Vertex Array Object first, then bind and set vertex buffer(s), and then configure vertex attributes(s).
//使用glBindVertexArray绑定VAO
glBindVertexArray(VAO);
//顶点缓冲对象的缓冲类型是:GL_ARRAY_BUFFER
//OpenGL允许我们同时绑定多个缓冲,只要他们是不同的缓冲类型
//我们使用glBindBuffer将新创建的缓冲,绑定到GL_ARRAY_BUFFER上
//从这一刻起,我们使用的任何(在GL_ARRAY_BUFFER目标上的)缓冲调用都会用来配置当前绑定的缓冲(VBO)。
glBindBuffer(GL_ARRAY_BUFFER, VBO);
//我们可以调用glBufferData函数,它会把之前定义的顶点数据复制到缓冲的内存中:
//第四个参数指定我们希望显卡如何管理给定的数据
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
//绑定索引缓冲对象,缓冲对象类型为:GL_ELEMENT_ARRAY_BUFFER
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
//通过glBufferData将索引复制到缓冲里
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
//通过glVertexAttribPointer告诉OpenGL如何解析顶点数据
//第一个参数为我们要配置的顶点属性,
// 还记得我们在顶点着色器中使用layout(location = 0)定义了position顶点属性的位置值(Location)吗?
// 它可以把顶点属性的位置值设置为0。
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
//我们现在应该使用glEnableVertexAttribArray,以顶点属性位置值作为参数,启用顶点属性;
//顶点属性默认是禁用的。自此,所有东西都已经设置好了:我们使用一个顶点缓冲对象将顶点数据初始化至缓冲中,
// 建立了一个顶点和一个片段着色器,
// 并告诉了OpenGL如何把顶点数据链接到顶点着色器的顶点属性上。在OpenGL中绘制一个物体,
//启用顶点属性
glEnableVertexAttribArray(0);
在渲染指定中加上代码:
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
这样我们就能完成矩形的绘制了
矩形绘制完整注释代码
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>
void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow *window);
// settings
//屏幕大小
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;
#pragma region 着色器源码
//顶点着色器源代码
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";
//片段着色器源代码
const char *fragmentShaderSource = "#version 330 core\n"
"out vec4 FragColor;\n"
"void main()\n"
"{\n"
" FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n"
"}\n\0";
#pragma endregion
int main()
{
#pragma region 窗口初始化与配置
// glfw: initialize and configure 初始化与配置
// ------------------------------
//glfw初始化
glfwInit();
//glfw配置
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
#ifdef __APPLE__
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif
#pragma endregion
// glfw window creation 创建窗口
// --------------------
GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
if (window == NULL)
{
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
//回调函数
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
// glad: load all OpenGL function pointers 通过glad管理所有的OpenGL函数指针
// ---------------------------------------
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
#pragma region shader
// build and compile our shader program 创建和编码 着色器程序
// ------------------------------------
// vertex shader 顶点着色器
//创建一个着色器对象
//通过glCreateShader函数进行着色器创建,输入参数为着色器类型
unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);
//把这个着色器源码附加到着色器对象上,然后编译它
//参数:把要编译的着色器对象作为第一个参数。第二参数指定了传递的源码字符串数量
//这里只有一个。第三个参数是顶点着色器真正的源码,第四个参数我们先设置为NULL。
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
//编译着色器
glCompileShader(vertexShader);
// check for shader compile errors
//检查着色器编译是否成功
int success;
char infoLog[512];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
}
// fragment shader 片段着色器
//通过glCreateShader函数进行着色器创建
unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
//将片段着色器附加到着色器对象上,然后编译它
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
//编译着色器
glCompileShader(fragmentShader);
// check for shader compile errors
//检查着色器是否编译成功
glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;
}
// link shaders 链接着色器
//创建一个着色器程序
//着色器程序对象(Shader Program Object)是多个着色器合并之后并最终链接完成的版本。
//如果要使用刚才编译的着色器我们必须把它们链接(Link)为一个着色器程序对象,
//然后在渲染对象的时候激活这个着色器程序。已激活着色器程序的着色器将在我们发送渲染调用的时候被使用。
unsigned int shaderProgram = glCreateProgram();
//将顶点着色器附加到着色器程序上
glAttachShader(shaderProgram, vertexShader);
//将片段着色器附加到着色器程序上
glAttachShader(shaderProgram, fragmentShader);
//使用glLinkProgram链接这些着色器
glLinkProgram(shaderProgram);
// check for linking errors
//检查 链接着色器是否成功
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if (!success) {
glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
}
// 将着色器连接到程序对象之后,删除着色器对象
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
glUseProgram(shaderProgram);
#pragma endregion
// set up vertex data (and buffer(s)) and configure vertex attributes
//顶点输入
// ------------------------------------------------------------------
// float vertices[] = {//三角形
// -0.5f, -0.5f, 0.0f, // left
// 0.5f, -0.5f, 0.0f, // right
// 0.0f, 0.5f, 0.0f // top
// };
//为了绘制矩形,我们可以通过绘制两个三角形,来绘制举行
//但是会造成产生两个顶点的额外开销,当这个数量变大后,会造成大量的资源浪费
// float vertices[] = {
// // 第一个三角形
// 0.5f, 0.5f, 0.0f, // 右上角
// 0.5f, -0.5f, 0.0f, // 右下角
// -0.5f, 0.5f, 0.0f, // 左上角
// // 第二个三角形
// 0.5f, -0.5f, 0.0f, // 右下角
// -0.5f, -0.5f, 0.0f, // 左下角
// -0.5f, 0.5f, 0.0f // 左上角
// };
//因此,只储存不同的顶点,并设定绘制这些顶点的顺序。这样子我们只要储存4个顶点就能绘制矩形了,之后只要指定绘制的顺序就行了。
float vertices[] = {
0.5f, 0.5f, 0.0f, // 右上角
0.5f, -0.5f, 0.0f, // 右下角
-0.5f, -0.5f, 0.0f, // 左下角
-0.5f, 0.5f, 0.0f // 左上角
};
unsigned int indices[] = { // 注意索引从0开始!
0, 1, 3, // 第一个三角形
1, 2, 3 // 第二个三角形
};
//创建VBO(顶点缓冲对象),VAO(顶点数组对象),EBO(索引缓冲对象)
unsigned int VBO;
unsigned int VAO;
unsigned int EBO;
//通过缓冲ID生成一个VAP对象
glGenVertexArrays(1, &VAO);
//使用glGenBuffers函数和一个缓冲ID生成一个VBO对象
glGenBuffers(1, &VBO);
//同样,通过glGenBuffers函数和缓冲ID生成EBO对象
glGenBuffers(1, &EBO);
// bind the Vertex Array Object first, then bind and set vertex buffer(s), and then configure vertex attributes(s).
//使用glBindVertexArray绑定VAO
glBindVertexArray(VAO);
//顶点缓冲对象的缓冲类型是:GL_ARRAY_BUFFER
//OpenGL允许我们同时绑定多个缓冲,只要他们是不同的缓冲类型
//我们使用glBindBuffer将新创建的缓冲,绑定到GL_ARRAY_BUFFER上
//从这一刻起,我们使用的任何(在GL_ARRAY_BUFFER目标上的)缓冲调用都会用来配置当前绑定的缓冲(VBO)。
glBindBuffer(GL_ARRAY_BUFFER, VBO);
//我们可以调用glBufferData函数,它会把之前定义的顶点数据复制到缓冲的内存中:
//第四个参数指定我们希望显卡如何管理给定的数据
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
//绑定索引缓冲对象,缓冲对象类型为:GL_ELEMENT_ARRAY_BUFFER
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
//通过glBufferData将索引复制到缓冲里
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
//通过glVertexAttribPointer告诉OpenGL如何解析顶点数据
//第一个参数为我们要配置的顶点属性,
// 还记得我们在顶点着色器中使用layout(location = 0)定义了position顶点属性的位置值(Location)吗?
// 它可以把顶点属性的位置值设置为0。
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
//我们现在应该使用glEnableVertexAttribArray,以顶点属性位置值作为参数,启用顶点属性;
//顶点属性默认是禁用的。自此,所有东西都已经设置好了:我们使用一个顶点缓冲对象将顶点数据初始化至缓冲中,
// 建立了一个顶点和一个片段着色器,
// 并告诉了OpenGL如何把顶点数据链接到顶点着色器的顶点属性上。在OpenGL中绘制一个物体,
//启用顶点属性
glEnableVertexAttribArray(0);
// note that this is allowed, the call to glVertexAttribPointer registered VBO as the vertex attribute's bound vertex buffer object so afterwards we can safely unbind
// glBindBuffer(GL_ARRAY_BUFFER, 0);
// You can unbind the VAO afterwards so other VAO calls won't accidentally modify this VAO, but this rarely happens. Modifying other
// VAOs requires a call to glBindVertexArray anyways so we generally don't unbind VAOs (nor VBOs) when it's not directly necessary.
// glBindVertexArray(0);
// uncomment this call to draw in wireframe polygons.
//glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
//OpenGL线框模式
// glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
// render loop
// -----------
while (!glfwWindowShouldClose(window))
{
// input
// -----
processInput(window);
// render
// ------
//清空并填充背景颜色
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// draw our first triangle
//激活着色器程序
//绑定顶点数组对象
// glBindVertexArray(VAO); // seeing as we only have a single VAO there's no need to bind it every time, but we'll do so to keep things a bit more organized
//它使用当前激活的着色器,之前定义的顶点属性配置,和VBO的顶点数据(通过VAO间接绑定)来绘制图元。
// glDrawArrays(GL_TRIANGLES, 0, 3);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
// glBindVertexArray(0); // no need to unbind it every time
// glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
// -------------------------------------------------------------------------------
glfwSwapBuffers(window);
glfwPollEvents();
}
// optional: de-allocate all resources once they've outlived their purpose:
// ------------------------------------------------------------------------
//
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
// glDeleteBuffers(1, &EBO);
glDeleteProgram(shaderProgram);
// glfw: terminate, clearing all previously allocated GLFW resources.
// ------------------------------------------------------------------
glfwTerminate();
return 0;
}
// process all input: query GLFW whether relevant keys are pressed/released this frame and react accordingly
// ---------------------------------------------------------------------------------------------------------
void processInput(GLFWwindow *window)
{
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
}
// glfw: whenever the window size changed (by OS or user resize) this callback function executes
// ---------------------------------------------------------------------------------------------
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
// make sure the viewport matches the new window dimensions; note that width and
// height will be significantly larger than specified on retina displays.
glViewport(0, 0, width, height);
}
/*
int main()
{
#pragma region windows
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);//MacOS系统
//参数分别为:长,宽,窗口标题,后面两个参数暂时忽略
GLFWwindow* window = glfwCreateWindow(800, 600, "MyFirstWindow", NULL, NULL);
if (window == NULL)
{
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
glViewport(0, 0, 800, 600);
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
// while(!glfwWindowShouldClose(window))
// {
// glfwSwapBuffers(window);
// glfwPollEvents();
// }
while (!glfwWindowShouldClose(window))
{
processInput(window);//通过esc关闭窗口
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glfwSwapBuffers(window);
glfwPollEvents();
}
glfwTerminate();
#pragma endregion
return 0;
}
*/
线框模式(Wireframe Mode)
要想用线框模式绘制你的三角形,你可以通过glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)
函数配置OpenGL如何绘制图元。第一个参数表示我们打算将其应用到所有的三角形的正面和背面,第二个参数告诉我们用线来绘制。之后的绘制调用会一直以线框模式绘制三角形,直到我们用glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
将其设置回默认模式。
相关文章
- Win64 驱动内核编程-12.回调监控进线程创建和退出
- 一起talk C栗子吧(第一百三十三回:C语言实例--创建进程时的内存细节)
- mysql中,创建包含json数据类型的表?创建json表时候的注意事项?查询json字段中某个key的值?
- IT咨询顾问:一次吐血的项目救火 java或判断优化小技巧 asp.net core Session的测试使用心得 【.NET架构】BIM软件架构02:Web管控平台后台架构 NetCore入门篇:(十一)NetCore项目读取配置文件appsettings.json 使用LINQ生成Where的SQL语句 js_jquery_创建cookie有效期问题_时区问题
- JS的web worker可以创建子线程
- 为nginx创建windows服务自启动
- 深入浅出 gRPC 02:gRPC 客户端创建和调用原理
- OpenShift一清二白的创建实例实战
- Fork/Join框架(二)创建一个Fork/Join池
- 创建 REST API 的最佳入门教程
- 如何创建圈子
- 《kafka问答100例 -3》 如果我没有指定分区数或者副本数,那么会如何创建
- SwiftUI进阶之 02 创建界面的三个思路 (《代码大全》学习笔记)
- JVM最多能创建多少个线程: unable to create new native thread
- React实践:Vite创建React项目、react事件传递参数的两种方式
- Java虚拟机(二)对象的创建与OOP-Klass模型
- 将google app engine 进行 本地化 可写文件 创建线程 去除白名单
- Camunda 创建springboot项目 (一)
- type 创建类,赋予类静态方法等
- 【tensorflow】——创建tensor的方法
- 《Linux命令行与shell脚本编程大全 第3版》创建实用的脚本---02
- vsCode 打开界面报错,尝试在目标目录创建文件时发生一个错误