参考网址:LearnOpenGL 中文版

4.7 几何着色器

4.7.1 基本概念

1、顶点和片段着色器之间有一个可选的几何着色器,几何着色器的输入是一个图元(如点或三角形)的一组顶点,顶点发送到下一着色器之前可对它们随意变换,将顶点变换为完全不同的图元,并且还能生成比原来更多的顶点。

#version 330 core
layout (points) in;
layout (line_strip, max_vertices = 2) out;void main() {    gl_Position = gl_in[0].gl_Position + vec4(-0.1, 0.0, 0.0, 0.0); EmitVertex();gl_Position = gl_in[0].gl_Position + vec4( 0.1, 0.0, 0.0, 0.0);EmitVertex();EndPrimitive();
}

2、在几何着色器的顶部,在in关键字前声明一个布局修饰符,声明从顶点着色器输入的图元类型,括号内的数字表示的是一个图元所包含的最小顶点数。

  • points:绘制GL_POINTS图元时(1)。
  • lines:绘制GL_LINES或GL_LINE_STRIP时(2)
  • lines_adjacency:GL_LINES_ADJACENCY或GL_LINE_STRIP_ADJACENCY(4)
  • triangles:GL_TRIANGLES、GL_TRIANGLE_STRIP或GL_TRIANGLE_FAN(3)
  • triangles_adjacency:GL_TRIANGLES_ADJACENCY或GL_TRIANGLE_STRIP_ADJACENCY(6)

3、在out关键字前面加一个布局修饰符,指定几何着色器输出的图元类型:

  • points
  • line_strip
  • triangle_strip

4、同时可以设置一个它最大能够输出的顶点数量(超过了这个值,OpenGL将不会绘制多出的顶点),使用上面定义的着色器输出一条线段,最大顶点数等于2:

5、几何着色器中有一个内建接口块变量gl_in[],包含了几个变量。其中,gl_Position与顶点着色器输出非常相似,它被声明为一个数组,因为几何着色器的输入是一个图元的所有顶点。

in gl_Vertex
{vec4  gl_Position;float gl_PointSize;float gl_ClipDistance[];
} gl_in[];

6、利用几何着色器函数EmitVertexEndPrimitive来生成新的数据了。调用EmitVertex时,gl_Position中的向量会被添加到图元中来。当EndPrimitive被调用时,所有发射出的顶点都会合成为指定的输出渲染图元。在一个或多个EmitVertex调用之后重复调用EndPrimitive能够生成多个图元。

void main() {gl_Position = gl_in[0].gl_Position + vec4(-0.1, 0.0, 0.0, 0.0); EmitVertex();gl_Position = gl_in[0].gl_Position + vec4( 0.1, 0.0, 0.0, 0.0);EmitVertex();EndPrimitive();
}

7、在这个例子中,接受一个点图元作为输入,从原始顶点位置平移了0.1,发射了两个顶点。之后调用EndPrimitive,将这两个顶点合成为一个包含两个顶点的线条。即以这个点为中心,创建一条水平的线图元。

glDrawArrays(GL_POINTS, 0, 4);

4.7.2 使用几何着色器

1、在标准化设备坐标的z平面上绘制四个点,包含顶点信息和颜色信息

float points[] = {-0.5f,  0.5f, 1.0f, 0.0f, 0.0f, // 左上0.5f,  0.5f, 0.0f, 1.0f, 0.0f, // 右上0.5f, -0.5f, 0.0f, 0.0f, 1.0f, // 右下-0.5f, -0.5f, 1.0f, 1.0f, 0.0f  // 左下
};

2、顶点着色器在z平面绘制点,使用一个接口块将颜色属性发送到几何着色器中。

#version 330 core
layout (location = 0) in vec2 aPos;
layout (location = 1) in vec3 aColor;out VS_OUT{vec3 color;
}vs_out;void main()
{gl_Position = vec4(aPos.x, aPos.y, 0.0, 1.0); vs_out.color = aColor;
}

3、创建一个传递几何着色器,接收点图元,在每个点的位置上绘制一个房子。每个顶点都是原始点的位置加上一个偏移量,来组成一个大的三角形带。使用三角形带(Triangle Strip)绘制,使用的顶点更少。在第一个三角形绘制完之后,每个后续顶点将会在上一个三角形边上生成另一个三角形,每3个临近的顶点将会形成一个三角形,得到(1, 2, 3)、(2, 3, 4)、(3, 4, 5)3个三角形。一个三角形带有N个顶点,会生成N-2个三角形。

4、几何着色器的输出

  • 将几何着色器的输出设置为triangle_strip
  • 在几何着色器中声明相同的接口块(使用一个不同的接口名VS_OUT )接受颜色属性,因为几何着色器是作用于输入的一组顶点的,从顶点着色器发来输入数据总是会以数组的形式表示出来。
  • 为片段着色器阶段声明一个输出颜色向量fColor,因为片段着色器只需要一个插值的颜色,就是一个单独的向量。
#version 330 core
layout (points) in;
layout (triangle_strip, max_vertices = 5) out;in VS_OUT {vec3 color;
} gs_in[];out vec3 fColor;void build_house(vec4 position)
{    fColor = gs_in[0].color; // gs_in[0] 因为只有一个输入顶点gl_Position = position + vec4(-0.2, -0.2, 0.0, 0.0);    // 1:左下EmitVertex();   gl_Position = position + vec4( 0.2, -0.2, 0.0, 0.0);    // 2:右下EmitVertex();gl_Position = position + vec4(-0.2,  0.2, 0.0, 0.0);    // 3:左上EmitVertex();gl_Position = position + vec4( 0.2,  0.2, 0.0, 0.0);    // 4:右上EmitVertex();gl_Position = position + vec4( 0.0,  0.4, 0.0, 0.0);    // 5:顶部EmitVertex();EndPrimitive();
}void main() {    build_house(gl_in[0].gl_Position);
}

5、创建着色器时使用GL_GEOMETRY_SHADER作为着色器类型,编译和链接几何着色器

geometryShader = glCreateShader(GL_GEOMETRY_SHADER);
glShaderSource(geometryShader, 1, &gShaderCode, NULL);
glCompileShader(geometryShader);
...
glAttachShader(program, geometryShader);
glLinkProgram(program);


6、也可以将最后一个顶点的颜色设置为白色,给屋顶落上一些雪。

fColor = gs_in[0].color;
gl_Position = position + vec4(-0.2, -0.2, 0.0, 0.0);    // 1:左下
EmitVertex();
gl_Position = position + vec4( 0.2, -0.2, 0.0, 0.0);    // 2:右下
EmitVertex();
gl_Position = position + vec4(-0.2,  0.2, 0.0, 0.0);    // 3:左上
EmitVertex();
gl_Position = position + vec4( 0.2,  0.2, 0.0, 0.0);    // 4:右上
EmitVertex();
gl_Position = position + vec4( 0.0,  0.4, 0.0, 0.0);    // 5:顶部
fColor = vec3(1.0, 1.0, 1.0);
EmitVertex();
EndPrimitive();

4.7.3 爆破物体

1、爆破一个物体是将每个三角形沿着法向量的方向移动一小段时间,看起来像是沿着每个三角形的法线向量爆炸一样。

2、在几何着色器中,沿着三角形的法向量位移每个顶点,首先使用3个顶点计算法向量,利用3个顶点计算两个平行于三角形表面的向量a和b,对这两个向量进行叉乘来获取法向量了。

vec3 GetNormal()
{vec3 a = vec3(gl_in[0].gl_Position) - vec3(gl_in[1].gl_Position);vec3 b = vec3(gl_in[2].gl_Position) - vec3(gl_in[1].gl_Position);return normalize(cross(a, b));
}

3、创建一个explode函数,使用法向量和顶点位置向量作为参数,令位置向量沿着法线向量进行位移。sin函数接收一个time参数,它根据时间返回一个-1.0到1.0之间的值。将sin值变换到了[0, 1]的范围内。最终的结果会乘以normal向量,并且最终的direction向量会被加到位置向量上。

vec4 explode(vec4 position, vec3 normal)
{float magnitude = 2.0;vec3 direction = normal * ((sin(time) + 1.0) / 2.0) * magnitude; return position + vec4(direction, 0.0);
}
shader.setFloat("time", glfwGetTime());

4、完整几何着色器,在发射顶点之前输出了对应的纹理坐标。

#version 330 core
layout (triangles) in;
layout (triangle_strip, max_vertices = 3) out;in VS_OUT {vec2 texCoords;
} gs_in[];out vec2 TexCoords; uniform float time;vec4 explode(vec4 position, vec3 normal) { ... }vec3 GetNormal() { ... }void main() {    vec3 normal = GetNormal();gl_Position = explode(gl_in[0].gl_Position, normal);TexCoords = gs_in[0].texCoords;EmitVertex();gl_Position = explode(gl_in[1].gl_Position, normal);TexCoords = gs_in[1].texCoords;EmitVertex();gl_Position = explode(gl_in[2].gl_Position, normal);TexCoords = gs_in[2].texCoords;EmitVertex();EndPrimitive();
}

4.7.4 法线可视化

1、思路是这样的:首先不使用几何着色器正常绘制场景。然后再次绘制场景,但这次只显示通过几何着色器生成法向量。几何着色器接收一个三角形图元,并沿着法向量生成三条线——每个顶点一个法向量。

shader.use();
DrawScene();
normalDisplayShader.use();
DrawScene();

2、使用模型提供的顶点法线,为了适配观察和模型矩阵的缩放和旋转,在将法线变换到裁剪空间坐标之前,先使用法线矩阵变换一次(几何着色器接受的位置向量是剪裁空间坐标,所以应该将法向量变换到相同的空间中)。

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;out VS_OUT {vec3 normal;
} vs_out;uniform mat4 projection;
uniform mat4 view;
uniform mat4 model;void main()
{gl_Position = projection * view * model * vec4(aPos, 1.0); mat3 normalMatrix = mat3(transpose(inverse(view * model)));vs_out.normal = normalize(vec3(projection * vec4(normalMatrix * aNormal, 0.0)));
}

3、变换后的裁剪空间法向量会以接口块的形式传递到下个着色器阶段。几何着色器会接收每一个顶点,并在每个位置向量处绘制一个法线向量。

#version 330 core
layout (triangles) in;
layout (line_strip, max_vertices = 6) out;in VS_OUT {vec3 normal;
} gs_in[];const float MAGNITUDE = 0.4;void GenerateLine(int index)
{gl_Position = gl_in[index].gl_Position;EmitVertex();gl_Position = gl_in[index].gl_Position + vec4(gs_in[index].normal, 0.0) * MAGNITUDE;EmitVertex();EndPrimitive();
}void main()
{GenerateLine(0); // 第一个顶点法线GenerateLine(1); // 第二个顶点法线GenerateLine(2); // 第三个顶点法线
}

4、片段着色器将它们显示为单色的线:

#version 330 core
out vec4 FragColor;void main()
{FragColor = vec4(1.0, 1.0, 0.0, 1.0);
}

4.8 实例化

4.8.1 基本概念

1、之前我们在渲染多个相同的物体时,都是使用循环的方法,但这样很快会因为绘制函数调用过多而达到性能瓶颈。因为OpenGL在绘制顶点数据之前需要做很多准备工作,比如:告诉GPU该从哪个缓冲读取数据,从哪寻找顶点属性,而且这些都是在相对缓慢的CPU到GPU总线上进行的。所以即便渲染顶点非常快,命令GPU去渲染却很慢。

for(unsigned int i = 0; i < amount_of_models_to_draw; i++)
{DoSomePreparations(); // 绑定VAO,绑定纹理,设置uniform等glDrawArrays(GL_TRIANGLES, 0, amount_of_vertices);
}

2、将数据一次性从CPU发送到GPU,然后使用一个绘制函数让OpenGL利用这些数据绘制多个物体,这就是实例化(Instancing)。

3、实例化使用一个渲染调用来绘制多个物体,来节省每次绘制物体时CPU -> GPU的通信。渲染函数为glDrawArraysInstancedglDrawElementsInstanced,函数需要设置渲染的实例个数。

3、利用上述函数渲染的每个物体都是完全相同的,而且还在同一个位置。所以需要利用顶点着色器中的内建变量gl_InstanceID。在使用实例化渲染调用时,gl_InstanceID会从0开始,在每个实例被渲染时递增1,每个实例都有唯一的ID,可以建立一个数组将ID与位置值对应起来,将每个实例放置在世界的不同位置。

4.8.2 实例化绘制

1、绘制100个2D四边形,索引一个包含100个偏移向量的uniform数组,将偏移值加到每个实例化的四边形上,得到一个排列整齐的四边形网格。

2、每个四边形由2个三角形所组成,一共有6个顶点。

float quadVertices[] = {// 位置          // 颜色-0.05f,  0.05f,  1.0f, 0.0f, 0.0f,0.05f, -0.05f,  0.0f, 1.0f, 0.0f,-0.05f, -0.05f,  0.0f, 0.0f, 1.0f,-0.05f,  0.05f,  1.0f, 0.0f, 0.0f,0.05f, -0.05f,  0.0f, 1.0f, 0.0f,   0.05f,  0.05f,  0.0f, 1.0f, 1.0f
};

3、片段着色器接受颜色向量,并将其设置为它的颜色输出:

#version 330 core
out vec4 FragColor;in vec3 fColor;void main()
{FragColor = vec4(fColor, 1.0);
}

4、顶点着色器中定义偏移向量offsets数组,使用gl_InstanceID来索引offsets数组,获取每个实例的偏移向量。

#version 330 core
layout (location = 0) in vec2 aPos;
layout (location = 1) in vec3 aColor;out vec3 fColor;uniform vec2 offsets[100];void main()
{vec2 offset = offsets[gl_InstanceID];gl_Position = vec4(aPos + offset, 0.0, 1.0);fColor = aColor;
}

5、在进入渲染循环之前设置偏移位置,创建100个位移向量,表示10x10网格上的所有位置

glm::vec2 translations[100];
int index = 0;
float offset = 0.1f;
for(int y = -10; y < 10; y += 2)
{for(int x = -10; x < 10; x += 2){glm::vec2 translation;translation.x = (float)x / 10.0f + offset;translation.y = (float)y / 10.0f + offset;translations[index++] = translation;}
}

6、将数据转移到顶点着色器的uniform数组中,将for循环的计数器i转换为一个string,用它来动态创建位置值的字符串,用于uniform位置值的索引。

shader.use();
for(unsigned int i = 0; i < 100; i++)
{stringstream ss;string index;ss << i; index = ss.str(); shader.setVec2(("offsets[" + index + "]").c_str(), translations[i]);
}

7、使用glDrawArraysInstanced函数:

glBindVertexArray(quadVAO);
glDrawArraysInstanced(GL_TRIANGLES, 0, 6, 100);

4.8.3 实例化数组

1、当需要渲染远超过100个实例的时候,会超过最大能够发送至着色器的uniform数据大小上限。因此使用实例化数组,将其定义为一个顶点属性。

2、顶点着色器的每次运行都会获取新一组顶点属性。将顶点属性定义为一个实例化数组时,顶点着色器就只需要对每个实例,更新顶点属性的内容。这允许我们对逐顶点的数据使用普通的顶点属性,而对逐实例的数据使用实例化数组。

3、将偏移量uniform数组设置为一个实例化数组。在顶点着色器中添加一个顶点属性:

#version 330 core
layout (location = 0) in vec2 aPos;
layout (location = 1) in vec3 aColor;
layout (location = 2) in vec2 aOffset;out vec3 fColor;void main()
{gl_Position = vec4(aPos + aOffset, 0.0, 1.0);fColor = aColor;
}

4、实例化数组和positioncolor变量一样,都是顶点属性,需要将它的内容存在顶点缓冲对象中,并且配置它的属性指针。首先将translations数组存到一个新的缓冲对象中:

unsigned int instanceVBO;
glGenBuffers(1, &instanceVBO);
glBindBuffer(GL_ARRAY_BUFFER, instanceVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(glm::vec2) * 100, &translations[0], GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, 0);

4、然后需要设置它的顶点属性指针,并启用顶点属性。调用了glVertexAttribDivisor函数定义什么时候更新顶点属性的内容至新一组数据。它的第一个参数是需要的顶点属性,第二个参数是属性除数。属性除数是0表示顶点着色器的每次迭代时更新顶点属性。属性除数1表示在渲染一个新实例的时候更新顶点属性。而设置为2时表示每2个实例更新一次属性。

glEnableVertexAttribArray(2);
glBindBuffer(GL_ARRAY_BUFFER, instanceVBO);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glVertexAttribDivisor(2, 1);

5、使用gl_InstanceID,从右上到左下逐渐缩小四边形,绘制的第一个四边形的实例会非常小,随着绘制实例的增加,gl_InstanceID会越来越接近100,四边形也就越来越接近原始大小。

void main()
{vec2 pos = aPos * (gl_InstanceID / 100.0);gl_Position = vec4(pos + aOffset, 0.0, 1.0);fColor = aColor;
}

4.8.4 小行星带

1、利用实例化渲染绘制一个行星带场景,所有的小行星都使用一个模型来表示,再使用不同的变换矩阵来进行少许的变化。

2、为每个小行星生成一个模型矩阵。首先将小行星的x和z位置变换到了一个半径为radius的圆形上,并且在半径的基础上偏移了-offset到offset。让y偏移的影响更小一点,让小行星带更扁平一点。接下来,应用了一个随机的缩放和旋转变换,并将最终的变换矩阵储存在modelMatrices中,这个数组的大小是amount。这里,我们一共生成1000个模型矩阵,每个小行星一个。

unsigned int amount = 1000;
glm::mat4 *modelMatrices;
modelMatrices = new glm::mat4[amount];
srand(glfwGetTime()); // 初始化随机种子
float radius = 50.0;
float offset = 2.5f;
for(unsigned int i = 0; i < amount; i++)
{glm::mat4 model;// 1. 位移:分布在半径为 'radius' 的圆形上,偏移的范围是 [-offset, offset]float angle = (float)i / (float)amount * 360.0f;float displacement = (rand() % (int)(2 * offset * 100)) / 100.0f - offset;float x = sin(angle) * radius + displacement;displacement = (rand() % (int)(2 * offset * 100)) / 100.0f - offset;float y = displacement * 0.4f; // 让行星带的高度比x和z的宽度要小displacement = (rand() % (int)(2 * offset * 100)) / 100.0f - offset;float z = cos(angle) * radius + displacement;model = glm::translate(model, glm::vec3(x, y, z));// 2. 缩放:在 0.05 和 0.25f 之间缩放float scale = (rand() % 20) / 100.0f + 0.05;model = glm::scale(model, glm::vec3(scale));// 3. 旋转:绕着一个(半)随机选择的旋转轴向量进行随机的旋转float rotAngle = (rand() % 360);model = glm::rotate(model, rotAngle, glm::vec3(0.4f, 0.6f, 0.8f));// 4. 添加到矩阵的数组中modelMatrices[i] = model;
}

2、在顶点着色器中,增加mat4的顶点属性,存储实例化数组的变换矩阵。顶点属性最大允许的数据大小等于一个vec4。因为一个mat4本质上是4个vec4,因此需要为这个矩阵预留4个顶点属性,矩阵每一列的顶点属性位置值就是3、4、5和6。

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 2) in vec2 aTexCoords;
layout (location = 3) in mat4 instanceMatrix;out vec2 TexCoords;uniform mat4 projection;
uniform mat4 view;void main()
{gl_Position = projection * view * instanceMatrix * vec4(aPos, 1.0); TexCoords = aTexCoords;
}

3、为这4个顶点属性设置属性指针,并将它们设置为实例化数组:

// 顶点缓冲对象
unsigned int buffer;
glGenBuffers(1, &buffer);
glBindBuffer(GL_ARRAY_BUFFER, buffer);
glBufferData(GL_ARRAY_BUFFER, amount * sizeof(glm::mat4), &modelMatrices[0], GL_STATIC_DRAW);for(unsigned int i = 0; i < rock.meshes.size(); i++)
{unsigned int VAO = rock.meshes[i].VAO;glBindVertexArray(VAO);// 顶点属性GLsizei vec4Size = sizeof(glm::vec4);glEnableVertexAttribArray(3); glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (void*)0);glEnableVertexAttribArray(4); glVertexAttribPointer(4, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (void*)(vec4Size));glEnableVertexAttribArray(5); glVertexAttribPointer(5, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (void*)(2 * vec4Size));glEnableVertexAttribArray(6); glVertexAttribPointer(6, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (void*)(3 * vec4Size));glVertexAttribDivisor(3, 1);glVertexAttribDivisor(4, 1);glVertexAttribDivisor(5, 1);glVertexAttribDivisor(6, 1);glBindVertexArray(0);
}

4、绘制小行星

instanceShader.use();
for(unsigned int i = 0; i < rock.meshes.size(); i++)
{glBindVertexArray(rock.meshes[i].VAO);glDrawElementsInstanced(GL_TRIANGLES, rock.meshes[i].indices.size(), GL_UNSIGNED_INT, 0, amount);
}

OpenGL学习笔记(十)-几何着色器-实例化相关推荐

  1. OpenGL蓝宝书第九章学习笔记:片段着色器和帧缓存

    前言 本篇在讲什么 OpenGL蓝宝书第九章学习笔记之片段着色器和帧缓存 本篇适合什么 适合初学OpenGL的小白 本篇需要什么 对C++语法有简单认知 对OpenGL有简单认知 最好是有OpenGL ...

  2. OpenGL基础41:几何着色器

    在顶点着色器之后,片段着色器之前,还有几何着色器,它是可选的,在<OpenGL基础3:渲染管线>这一章中就有提到了,有了几何着色器后可以做很多骚操作,更容易实现很多有意思的效果 一.最简单 ...

  3. 【OpenGL】笔记三、着色器

    1. 流程 1.1 图形颜色随时间变化 接上回生成矩形,本次我们要对矩形颜色做一定改变,让它在渲染过程中随时间而变化,但是目前来说我们好像无法在着色器源码中获取时间,那么剩下的方法应该就是在程序中对片 ...

  4. openGL学习笔记十四: 透视投影

    透视投影   三维空间物体显示近大远小. 透视投影区域是个棱锥体: 透视投影显示效果: opengl透视投影函数: void APIENTRY gluPerspective (GLdouble fov ...

  5. OpenGL学习笔记(十三):将纹理贴图应用到四边形上,对VAO/VBO/EBO/纹理/着色器的使用方式进行总结

    原博主博客地址:http://blog.csdn.net/qq21497936 本文章博客地址:http://blog.csdn.net/qq21497936/article/details/7919 ...

  6. Unity Shader:用几何着色器实现复联3灭霸的终极大招灰飞烟灭

    (图1:正常渲染) (图2:几何着色器粒子化特效进行中) (图3:几何着色器粒子化特效进行中) 1,用几何着色器进行图元转换 在OpenGL渲染管线中,几何着色器Geometry Shader有一个独 ...

  7. LearnOpenGL-高级OpenGL-9.几何着色器

    本人初学者,文中定有代码.术语等错误,欢迎指正 文章目录 几何着色器 使用几何着色器 造几个房子 爆破物体 法向量可视化 几何着色器 简介 在顶点和片段着色器之间有一个可选的几何着色器 几何着色器的输 ...

  8. Windows 8 Directx 开发学习笔记(十四)使用几何着色器实现三角形细分

    几何着色器是从DirectX 10才引入的着色器,是一个可选阶段,位于顶点着色器和像素着色器阶段之间.顶点着色器以顶点作为输入数据,而几何着色器以完整的图元作为输入数据,像点.直线.三角形等.之所以引 ...

  9. dx12 龙书第十二章学习笔记 -- 几何着色器

    如果不启用曲面细分(tessellation)这一环节,那么几何着色器(geometry shader)这个可选阶段便会位于顶点着色器与像素着色器之间.顶点着色器以顶点作为输入数据,而几何着色器的输入 ...

最新文章

  1. Oracle学习笔记:oracle的编程接口
  2. [AHOI2008] 紧急集合
  3. fck java_FCkjava三种调用方法
  4. 初识python: 字符编码转换
  5. 图表 stack属性_想快速了解MEAN Stack吗? 这是带有有用图表的文档。
  6. 今晚直播丨如何通过APEX实现数据库自动晨检功能
  7. springMVC学习(10)-上传图片
  8. (转)大多数2B企业一文不值(作者:北冥乘海生)
  9. SQLite jdbc驱动
  10. 【渗透测试基础-1】渗透测试方法论及渗透测试流程
  11. Struts2的拦截器
  12. c语言房屋中介系统,ZX房屋中介管理系统(毕设)源码
  13. oracle最小值寒素,新人教版备考2020年浙江中考语文复习专题:基础知识与古诗文专项特训(五十六)D卷...
  14. Blekko推出类Flipboard社交新闻网站ROCKZi
  15. Ubantu18.04环境下编译android源码
  16. 查看手机的mac地址
  17. 1W字看懂互联网知识经济
  18. docker: error pulling image configuration:timeout
  19. Ubuntu 10.04内核源码树的编译和安装
  20. 南怀瑾:“心静出贵人”,中年后这三个地方静,一切都会越来越顺!

热门文章

  1. 《聊斋Q传》地狱问答系统的设计疏漏与改进
  2. OSChina 周一乱弹 ——程序员的弱点!
  3. 输入压缩空间量是分区量吗_硬盘只有C盘如何增加分区
  4. knn算法代码详解(以鸢尾花数据为例)
  5. iOS 快速制作APP屏幕快照 轻图
  6. 为了混圈子,她花光20万积蓄买爱马仕包:你没价值,混进再牛的圈子也没用...
  7. C++之数据类型转换(全)
  8. 最简单的sql多表联查语句
  9. aspire鹦鹉螺_【详评兔】Aspire Nautilus 2 鹦鹉螺2成品雾化器 | 经典传承
  10. html 像素设计,网页应该设计为多少像素才合适?