openGL 学习笔记 (二) 使用GL API 绘制出属于自己的矩形
在OpenGL中所有的事物都是在3D空间中,但是我们所看到的屏幕成像却是2D的像素数组。这导致OpenGL的大部分工作就是把得到的3D坐标转换为适应屏幕的2D图像。转换的整个处理过程是由OpenGL的图形渲染管线管理的。
OpenGL图形渲染管线:
1> 指的是一堆原始图形数据途经一个输送管道,期间经过各种变化处理最终出现在屏幕的过程
2> 图形渲染管线可以被划分为两个主要部分:第一部分把你的3D坐标转换为2D坐标,第二部分是把2D坐标转变为实际的有颜色的像素。
3> 图形渲染管线可以划分为几个阶段,每个阶段都是把上个阶段的输出当做输入来处理的。由于每隔阶段的处理都是高度专门化的,所以很容易并行执行,所以在GPU中可用成千上万的小处理核心可以为每个管线阶段的着色器并行处理。
4> OpenGL的渲染管线大致可分为以下几个阶段:
定点数据输入——> >图元(片元)装配——>几何着色器——>光栅化——>片段着色器——>Alpha测试与颜色混合
5> 有些着色器语序可以通过开发者自己实现,从而达到更细致的控制图像的渲染结果和速度。其中顶点着色器,几何着色器,片段着色器是我们可以重新实现的部分。
简略概括:
顶点数据输入: 首先将顶点数据以数组的形式将n个3D坐标作为整个图形渲染管线的输入。顶点数据是一系列顶点的集合。一个顶点是一个3D坐标的数据的集合。在OpenGL中顶点数据是用顶点属性表示的。他可以包含任何可能呢会用到的数据。
顶点着色器: 接受一个单独的顶点进行输入,主要工作是将3D坐标变为另一种3D坐标,他接受开发者自己实现。
图元(片元)装配:
1> OpenGL需要去指定我们想要绘制的图像的坐标和颜色构成,是一系列的点,或是一系列的三角形,还是一系列的线。这些最基础的构成被称为图元(片元)。 // TODO eg:
2> 图元装配将顶点着色器的所有输出作为输入,(注意,如果图元类型为GL_PIONTS,则只接受一个顶点)。然后将所有的点装配成指定好的图元形状。
几何着色器: 接受图元形式的一系列顶点作为输入,通过产生新顶点构造出新的图元生成其他图元。
光栅化: 将图元映射为最终屏幕上相应的像素,生成供片段着色器使用的片段,并进行裁切,将超出你的视图以外的所有像素丢弃,用来提升执行效率。
片段着色器:
1> 这里的片段是指OpenGL渲染一个像素所需的所有数据。
2> 片段着色器将计算一个像素的最终颜色,这也是所有OpenGL高级效果产生的地方。通常,片段着色器包含3D场景的数据(如光照、阴影、光的颜色等),这些数据可以被用来计算该片段最终像素的颜色。
Alpha测试与颜色混合: 该阶段会检测对应片段对应的深度值,用来判断这个片段是否存在于其他的片段之前或者之后,从而判断该片段是否要被丢弃。同时也会检查Alpha(透明度)值并对片段进行颜色混合
详细过程及代码实现:
定义顶点数据
float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
(OpenGL仅当3D坐标在3个轴(x、y和z)上都为-1.0到1.0的范围内时才处理它。)
将定义的定点数据作为输入发送给顶点着色器,顶点着色器在GPU上创建内存用于储存顶点数据,还要配置OpenGL如何解释这些内存,并且指定其如何发送给显卡,顶点着色器接着会处理我们在内存中指定好数量的顶点。
这个时候如果一个顶点一个顶点的发送,其缺点是显而易见的,将顶点数据从CPU发送至GPU的速度相对较慢。使用顶点缓冲对象来管理这些顶点的内存则可以一次性发送尽可能多的数据。当顶点数据发送至显存中后,顶点着色器几乎能立即访问顶点。
创建一个顶点缓冲对象的过程如下:
首先生成一个缓冲区对象
// generate buffer object names
void glGenBuffers(GLsizei n, GLuint * buffers);
第一个参数是要生成的缓冲对象的数量,第二个是要输入用来存储缓冲对象名称的长度为n的id数组,如果 n == 1, 则只需要一个id。
该函数仅仅生成一个缓冲对象的名称,这个缓冲对象并不具备任何意义,即它仅仅是个缓冲对象,还不是顶点缓冲对象。他就像C中的一个指针变量,可以给他分配内存对象并且用它的名称来引用这个内存对象。
unsigned int VBO;
glGenBuffers(1, &VBO);
顶点缓冲对象的缓冲区类型(缓冲区ID)为 GL_ARRAY_BUFFER,
要指定缓冲对象的类型则需要用到下面的函数
// bind a named buffer object
void glBindBuffer(GLenum target, GLuint buffer);
一个参数是缓冲对象的类型,第二个参数是已经创建的缓冲对象的名称。使用该函数将创建好的VBO对象绑定到OpenGL的上下文环境中,如果绑定的buffer值为零,那么OpenGL将不再对当前target使用任何对象。
需要注意的是函数的第二个参数虽然是Gluint型的。
OpenGL允许我们同一同时绑定多个不同类型的缓冲对象,但是不能绑定两个相同的缓冲对象,这就是上下文之所以用上下文来理解它的意义。
缓冲对象只是OpenGL对象的一种,使用其他对象的思路跟缓冲对象是差不多的,都是创建对象——>绑定上下文——>设置数据
// 创建并初始化缓冲区对象的数据存储
void glBufferData (GLenum target, GLsizeiptr size, const GLvoid * data, GLenum usage);
该函数将删除之前存在于指定类型缓冲区的数据,为当前已绑定的指定类型的缓冲区对象创建一个新的数据存储,第一个参数是指定目标缓冲区类型,有GL_ARRAY_BUFFER(VBO)、GL_ELEMENT_ARRAY_BUFFER(EBO)两种,第二个参数用来指定缓冲区对象的新数据需要的内存大小,第三个参数是我们希望发送的实际数据的指针,第四个参数用来指定给GPU数据的预期使用方式,这使得GL的实现能够做出更明智的选择,可能会影响缓冲区对象的性能,它不会限制数据存储的实际使用。usage可分解为两个部分理解。第一,访问的频率,第二,访问的性质:
访问频率:
STREAM // 数据存储内容将被修改一次并最多使用几次。
STATIC // 数据存储内容将被修改一次并多次使用。
DYNAMIC // 数据存储内容将被重复修改并多次使用。
访问的性质
DRAW // 数据存储内容由应用程序修改,并用作GL绘图和图像规范命令的源。
和在一起则是以下三个
GL_STATIC_DRAW // 数据不会或几乎不会改变。
GL_DYNAMIC_DRAW // 数据会被改变很多。
GL_STREAM_DRAW // 数据每次绘制时都会改变。
到此顶点数据已经被存在GPU内存中了。
顶点着色器:
顶点着色器就要使用着色器语言GLES,用字符串存储,发送给GL,然后编译这个着色器。eg:
#version 330 core
layout (lcoation = 0) in vec3 aPos;
void main()
{
//注意,如果图元类型为GL_PIONTS,则只接受一个顶点
sition = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}
每个着色器都起始与一个版本声明,同时说明我们使用的渲染模式是核心模式。
in 关键字用来顶点着色器中声明的所有顶点属性,(上面的只有一个顶点位置属性),layout (lcoation = 0) 代表输入变量的位置值。
在主函数中设置着色器的双输出值,这里将gl_Position顶点位置属性进行更改。
然后是编译着色器:
// 创建一个着色器对象
unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER); // 将着色器源码附加到着色器对象上
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL)
// 第一个参数是创建好的着色器对象,第二个参数指定的传递的源码字符串数量,第三个参数是顶点着色器所需要的源码。 // 编译着色器
glCompileShader(vertexShader); // 检测着色器是否编译编译成功,并获取错误信息。
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;
}
片段着色器:
片段着色器的编写,编译和顶点着色器的过程大同小异,只是类型变成了GL_FRAGMENT_SHADER。
#version 330 core
out vec4 FragColor;
void main()
{
FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
}
// 片段着色器只需要一个输出变量,这个变量是一个vec4类型,他代表的是最终的 输出颜色,使用out关键字声明输出变量。
unsigned int fragmentShader;
fragmentShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
两个着色器写完之后,将两个着色器对象链接到一个用来渲染的着色器程序中。
着色器程序对象是多个着色器对象链接在一起的成果。在渲染对象的时候激活需要的着色器程序。在发送渲染调用的时候GL会使用当前激活的着色器程序。
当链接着色器至一个程序的时候,它会把每个着色器的输出链接到下个着色器的输入。当输出和输入不匹配的时候,你会得到一个连接错误。
// 创建一个着色器程序对象
// 创建一个着色器程序对象
unsigned int shaderProgram;
shaderProgram = glCreateProgram(); // 将着色器对象加入着色器程序对象并链接
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram); // 检查链接成功:
int successs;
char infoLog[512];
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if(!success) {
glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
} // 在链接成功后删除之前创建的着色器对象,释放内存
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader); // 在需要的时候,使用着色器
glUseProgram(shaderProgram);
在glUseProgram函数调用之后,每个着色器调用和渲染调用都会使用这个着色器程序对象。
顶点数据已经发送给了GPU,也指示了GPU在顶点着色器和片段着色器中如何处理它,但是GPU还不知道如何处理已经存在GPU内存中的的顶点数据。即如何将已经在GPU中的顶点是数据以顶点着色器可以接受的方式发送给顶点着色器。顶点着色器可以接受任何以顶点属性形式的输入,所以顶点着色器有着很强的灵活性,但是这样我们必须手动指定输入数据的哪一个部分对应顶点着色器需要的的哪一个顶点属性。所以在开始渲染之前,必选要告诉顶点着色器如何解释顶点数据。
上文在传入的时候,vertices是一个float类型的一维数组,即一段连续的浮点型内存,在GPU中顶点缓冲数据则会被解析为以下特征的连续内存:
1> 每个数据会被存储32位(4字节)的float类型值;
2> 每个位置包含三个这样的值;
3> 存储的内存和传入的时候一样是连续的;
4> 数据的第一个值在缓冲开始的位置,即数组的第0个元素。
通过既定的这个信息,就可以使用glVertexAttribPointer函数来告诉GL如何去解释顶点缓冲对象。eg:
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 参数含义
1> 第一个参数指定配置的顶点属性,在编写顶点着色器的时候,第二句中有个 layout(location = 0),该语句设置position顶点的属性位置值(location)为0,如果希望把顶点数据传递到这一个顶点属性中,那么这里参数传入0;
2> 第二个参数用来指定顶点属性的大小,因为用到的顶点位置数据为vec3,由三个值组成,大小为3;
3> 第三个参数指定数据的类型;
4> 第四个参数指定是否希望数据被标准化,如果为GL_TRUE,所有传入的数据会被映射到0到1(有符号输为-1到1)之间。
5> 第五个参数可以用“步长”来解释,他用来告诉GL整个顶点属性第二次出现的地方到整个数组0位置之间有多少字节,也可以传入0让GL自己检测,但是只能在传入连续内存(即数组)的情况下使用。
6> 第六个参数用来表示位置数据在缓冲中起始位置的偏移量(Offset),但要注意,改参数的类型为void*,所以要讲类型强制转换为void*。
而 glEnableVertexAttribArray 则以顶点属性位置值作为参数,启用顶点属性,因为顶点属性默认是禁用的。 整套流程下来大概会是这样:
// 创建并绑定顶点缓冲对象
float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
}; glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 设置顶点属性指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0); // 渲染时,使用着色器程序
glUseProgram(shaderProgram); // 绘制物体
someOpenGLFunctionThatDrawsOurTriangle();
在绘制时,每绘制一个物体的时候都必须重复这一过程,这个时候绑定正确的缓冲对象,为每次绘制配置正确的顶点属性就变的比较麻烦,为了解决这个麻烦,可以使用顶点数组对象,将所有绘制需要的状态信息储存在一个对象中。
顶点数组对象可以像顶点缓冲对象一样被绑定,任何随后的顶点属性调用都会被储存着这个VAO对象中,这样就等于说是在配置顶点属性指针时,只需要将之前的调用执行一次,在绘制不同的物体时,只需要绑定不同的VAO就可以了。(OPGL的核心渲染模式指定使用VAO对象,如果绑定VAO对象失败,OPGL会拒绝渲染)
一个顶点数组对象将会存储以下内容:
1> glEnableVertexAttribArray和glDisableVertexAttribArray的调用。
2> 通过glVertexAttribPointer设置的顶点属性配置。(也就是数据放在哪里,和如何读取数据)
3> 通过glVertexAttribPointer调用与顶点属性关联的顶点缓冲对象。
在内存中大概就是这个意思
VBO1 = {pos[0], pos[1], pos[2], ...}
VBO2 = {pos2[0], pos2[1], pos2[2], ...}
VAO1 = {*pos[0]} VBO3 = {pos[0], color[0], pos[1], color[1], pos[2], color[2], ...}
VAO2 = {*pos[0], *color[0]}
创建一个VAO对象跟创建一个VBO对象相似:
unsigned int VAO;
glGenVertexArrays(1, &VAO);
在使用glBindVertexArray绑定VAO之后,就可以对VAO对象进行操作。在绑定之后,可把绑定和配置对应的VBO和属性指针然后解绑,供之后使用。在绘制物体的时候只需要把VAO对象绑定到希望使用的设定上,新的使用VAO绘制的流程应该如下:
// 绑定VAO
glBindVertexArray(VAO); // 将BO复制到GL缓冲区中供GL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 设置VBO对象
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0); // 绘制
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
someOpenGLFunctionThatDrawsOurTriangle(); // 实际绘制时(在循环中)使用的代码应如下
glUseProgram(shaderProgram);
glBindVertexArray(VAO); //绑定我们需要的VAO,会导致上面所有VAO保存的设置自动设置完成
glDrawArrays(); // 最后记得解绑不会再使用的VAO对象
glBindVertexArray(0); //解绑VAO
索引缓冲对象(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, // 右下角
-0.5f, -0.5f, 0.0f, // 左下角
-0.5f, 0.5f, 0.0f // 左上角
}
可以看到,有两对一模一样的顶点,但实际上,一个矩形只有四个顶点这就会产生50%的额外开销。如果模型越来愈大,这个问题会更加糟糕,会产生数不清的浪费。好的解决方案是存储不同的顶点,并设定绘制这些顶点的顺序。这就是索引缓冲对象的(EBO)工作方式和顶点缓冲对象一样EBO是一个缓冲,他专门储存索引,OpenGL调用这些顶点的索引来决定绘制哪个点。
1> 首先,我们要定义不懂位置的顶点,和绘制矩形所需要啊的索引:
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 // 第二个三角形
};
2> 创建索引缓冲对象:
ussigned int EBO;
glGenBuffers(1, &EBO);
3> 绑定EBO然后用将索引复制到缓冲中
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
4> 用glDrawElements来替换glDrawArrays函数,来指明我们从索引缓冲渲染。
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
第一个参数指定了我们绘制的模式;
第二个参数是我们打算绘制顶点的个数,这里填6;
第三个参数是索引的类型;
最后一个参数里我们指定EBO中的偏移量。
如果整套流程下来(使用VAO、VBO、EBO)整个三角形的位置代码会是这样:
#include <iostream>
#include <glad/glad.h>
#include <GLFW/glfw3.h> #include "config.h"
using namespace std; const char *strvertexShaderSource = "#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 *strfragmentShaderSource = "#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"; 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(WIND_WIGHT, WIND_HEIGHT, "window", NULL, NULL);
if (window == NULL)
{
std::cout << "create window failed!" << endl;
glfwTerminate();
return -1;
} glfwMakeContextCurrent(window); if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "init glad failed!" << endl;
return -1;
} // 创建一个着色器,着色器类型为 GL_VERTEX_SHADER(顶点着色器)
unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);
// 把着色器源码附加到着色器对象
glShaderSource(vertexShader, 1, &strvertexShaderSource, NULL);
// 编译着色器
glCompileShader(vertexShader); int success;
char infoLog[512];
// 判断编译结果并打印出错误
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
std:cout << "VERTEX SHADER COMPILATION_FAILED" << infoLog << std::endl;
} // 创建一个着色器,着色器类型为 GL_FRAGMENT_SHADER (片段着色器)
unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &strfragmentShaderSource, NULL);
glCompileShader(fragmentShader); // 判断编译结果并打印出错误
glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);
std::cout << "FRAGMENT SHADER COMPLATION_FAILED" << infoLog << std::endl;
} // 链接着色器对象
int shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram); // 判断链接结果并打印出错误
if (!success)
{
glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
std::cout << "LINK SHADER FAILED" << infoLog << std::endl;
} // 链接成功后就可以将shader删除
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader); // 将顶点数据传入顶点着色器
// 定义一个顶点数组
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, 1, 3,
1, 2, 3
}; // 创建一个VAO对象,一个VBO对象,一个EBO对象
unsigned int VAO, VBO, EBO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glGenBuffers(1, &EBO); // 绑定VAO对象
glBindVertexArray(VAO); // 将VBO复制到GL缓冲区中供GL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO); // 复制顶点数组到缓冲中供OpenGL使用
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 告诉GL如何去解释顶点缓冲对象(VBO)
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); // 将EBO复制到GL缓冲区中供GL使用
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); // 将顶点索引复制到缓冲中
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); // 告诉GL启用顶点属性
glEnableVertexAttribArray(0); // 解除上下文绑定VAO,这样其他VAO调用不会意外地修改此VAO(通常情况下是不必要的)
// glBindVertexArray(0); while (!glfwWindowShouldClose(window))
{
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT); // 使用链接好的shader
glUseProgram(shaderProgram); // 绑定VAO对象(如果有操作解绑了VAO对象)
// glBindVertexArray(VAO); // 指明从索引缓冲渲染
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); glfwSwapBuffers(window);
glfwPollEvents();
} // GC
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
glDeleteBuffers(1, &EBO);
glDeleteProgram(shaderProgram); glfwTerminate(); return 0;
}
openGL 学习笔记 (二) 使用GL API 绘制出属于自己的矩形的更多相关文章
- Java 学习笔记 (二) Selenium WebDriver Java 弹出框
下面这段实例实现了以下功能: 1. profile使用用户本地电脑上的 (selenium 3有问题.因为selenium 3把profile复制到一个temp文件夹里,但并不复制回去.所以每次打开仍 ...
- ZooKeeper学习笔记二:API基本使用
Grey ZooKeeper学习笔记二:API基本使用 准备工作 搭建一个zk集群,参考ZooKeeper学习笔记一:集群搭建. 确保项目可以访问集群的每个节点 新建一个基于jdk1.8的maven项 ...
- OpenGL学习笔记3——缓冲区对象
在GL中特别提出了缓冲区对象这一概念,是针对提高绘图效率的一个手段.由于GL的架构是基于客户——服务器模型建立的,因此默认所有的绘图数据均是存储在本地客户端,通过GL内核渲染处理以后再将数据发往GPU ...
- OpenGL学习笔记:拾取与选择
转自:OpenGL学习笔记:拾取与选择 在开发OpenGL程序时,一个重要的问题就是互动,假设一个场景里面有很多元素,当用鼠标点击不同元素时,期待作出不同的反应,那么在OpenGL里面,是怎么知道我当 ...
- AJax 学习笔记二(onreadystatechange的作用)
AJax 学习笔记二(onreadystatechange的作用) 当发送一个请求后,客户端无法确定什么时候会完成这个请求,所以需要用事件机制来捕获请求的状态XMLHttpRequest对象提供了on ...
- java之jvm学习笔记二(类装载器的体系结构)
java的class只在需要的时候才内转载入内存,并由java虚拟机的执行引擎来执行,而执行引擎从总的来说主要的执行方式分为四种, 第一种,一次性解释代码,也就是当字节码转载到内存后,每次需要都会重新 ...
- NumPy学习笔记 二
NumPy学习笔记 二 <NumPy学习笔记>系列将记录学习NumPy过程中的动手笔记,前期的参考书是<Python数据分析基础教程 NumPy学习指南>第二版.<数学分 ...
- ES6学习笔记<二>arrow functions 箭头函数、template string、destructuring
接着上一篇的说. arrow functions 箭头函数 => 更便捷的函数声明 document.getElementById("click_1").onclick = ...
- python3.4学习笔记(二十三) Python调用淘宝IP库获取IP归属地返回省市运营商实例代码
python3.4学习笔记(二十三) Python调用淘宝IP库获取IP归属地返回省市运营商实例代码 淘宝IP地址库 http://ip.taobao.com/目前提供的服务包括:1. 根据用户提供的 ...
- qml学习笔记(二):可视化元素基类Item详解(上半场anchors等等)
原博主博客地址:http://blog.csdn.net/qq21497936本文章博客地址:http://blog.csdn.net/qq21497936/article/details/78516 ...
随机推荐
- Python中的枚举类enum
0. 本文来历 上一篇文章,我写了Pytest插件pytest-order指定用例顺序 我当时就比较好奇它的顺序和英文的对应关系,肯定是写死的,找了下就发现在源码sorter.py中定义了一个dict ...
- 算法学习笔记(17): 快速傅里叶变换(FFT)
快速傅里叶变换(FFT) 有趣啊,都已经到NOI的难度了,救命 首先,我们先讲述一下前置知识.已经明白的读者请移步后文 虚数 定义:\(z = a + bi\),其中 \(a, b \in R\ \ ...
- 线程基础知识14 ReentrantLock和ReentrantReadWriteLock
1 简介 ReentrantLock和ReentrantReadWriteLock都是可重入锁.可重入锁,顾名思义,就是支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁 ReentrantL ...
- 使用Hook拦截sendto函数解决虚拟局域网部分游戏联机找不到房间的问题——以文明6为例
正文 重要提醒(2023-02-13):本文部分内容存在bug,目前正在调试修改,会在一段时间之后更新 重要提醒(2023-02-14):目前已修复主要bug,会在一段时间之后更新,本文计划重写大部分 ...
- 学习ASP.NET Core Blazor编程系列二十六——登录(5)
学习ASP.NET Core Blazor编程系列文章之目录 学习ASP.NET Core Blazor编程系列一--综述 学习ASP.NET Core Blazor编程系列二--第一个Blazor应 ...
- 关于移动端使用echarts点击图标外部不能关闭tooltip的问题
新建一个mixin文件 粘贴如下代码: 1 /** 2 * 1. 需要将echart实例赋值为 this.echartsInstance `echartsInstance` echarts 带s 3 ...
- 利用canvas+js完成滑块验证码中canvas部分思路
1. 最终效果 2.滑块验证码思路 大概思路:设置两个画布,一个为显示图像的canvas画布,一个为拼图的block画布,block画布拼图内容从图像画布中的一部分裁剪得到(使用clip()),通过绑 ...
- 18 网路进阶设定:Bridge、LACP、VLAN
18 网路进阶设定:Bridge.LACP.VLAN 18.1 建立第二网路桥接装置(Bridge) 在预设安装完的情况下,PVE会使用其中一个连接埠桥接至[vmbr0]这个预设的网路桥接装置,所有的 ...
- wordpress宕机原因及处理方法
2020年7月底,查看了网站日志,是wp-cron.php 导致异常. 原来这是WordPress定时任务,禁用即可. 在wp-config.php添加 /* 禁用定时任务 wp-cron */ de ...
- react的useRef
在使用RN动画的时候,看到这样的代码: const App = () => { const fadeAnim = useRef(new Animated.Value(0)).current; / ...