当前位置: 首页 > news >正文

学网站设计和平面设计wordpress-5.6.20下载

学网站设计和平面设计,wordpress-5.6.20下载,江门鹤山最新消息新闻,c2c模式的优势和劣势教程地址#xff1a;简介 - LearnOpenGL CN 高级数据 原文链接#xff1a;高级数据 - LearnOpenGL CN 在OpenGL中#xff0c;我们长期以来一直依赖缓冲来存储数据。本节将深入探讨一些操作缓冲的高级方法。 OpenGL中的缓冲本质上是一个管理特定内存块的对象#xff0c;它…教程地址简介 - LearnOpenGL CN 高级数据 原文链接高级数据 - LearnOpenGL CN 在OpenGL中我们长期以来一直依赖缓冲来存储数据。本节将深入探讨一些操作缓冲的高级方法。 OpenGL中的缓冲本质上是一个管理特定内存块的对象它本身没有更多的功能。只有当我们将其绑定到一个缓冲目标(Buffer Target)时才能赋予其具体的用途。例如当一个缓冲被绑定到 GL_ARRAY_BUFFER 时它用于存储顶点数组而当绑定到 GL_ELEMENT_ARRAY_BUFFER 时则用于索引数据。OpenGL为每个目标维护一个独立的缓冲并根据目标的不同以相应的方式处理它们。 为了填充缓冲对象所管理的内存我们通常使用 glBufferData 函数 原型void glBufferData(GLenum target, GLsizeiptr size, const void *data, GLenum usage);作用分配指定大小的GPU内存并将数据添加进去。如果 data 参数设置为NULL则只分配内存而不填充这在需要预留特定大小内存时非常有用。示例glBufferData(GL_ARRAY_BUFFER, sizeof(skyboxVertices), skyboxVertices, GL_STATIC_DRAW); 另一种填充缓冲的方法是使用glBufferSubData 原型void glBufferSubData(GLenum target, GLintptr offset, GLsizeiptr size, const void *data);作用更新现有缓冲的特定区域。通过提供偏移量可以从指定位置开始填充或更新缓冲的部分内容。注意在调用此函数之前必须先确保有足够的已分配内存调用glBufferData。示例glBufferSubData(GL_ARRAY_BUFFER, 24, sizeof(data), data); // 范围 [24, 24 sizeof(data)] 若要直接请求缓冲区的内存指针进行数据复制可以使用 glMapBuffer 和 glUnmapBuffer glMapBuffer 原型void* glMapBuffer(GLenum target, GLenum access);作用请求并返回当前绑定缓冲的内存指针允许用户直接操作该内存。在调用此函数之前必须先确保有足够的已分配内存调用glBufferData。glUnmapBuffer 原型GLboolean glUnmapBuffer(GLenum target);作用通知OpenGL已完成对映射缓冲的操作解除映射并确保数据成功写入缓冲。在解除映射(Unmapping)之后指针将会不再可用并且如果OpenGL能够成功将您的数据映射到缓冲中这个函数将会返回GL_TRUE。示例float data[] {0.5f, 1.0f, -0.35f... }; glBindBuffer(GL_ARRAY_BUFFER, buffer); // 获取指针 void *ptr glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY); // 复制数据到内存 memcpy(ptr, data, sizeof(data)); // 记得告诉OpenGL我们不再需要这个指针了 glUnmapBuffer(GL_ARRAY_BUFFER);如果要直接映射数据到缓冲而不事先将其存储到临时内存中glMapBuffer这个函数会很有用。比如说你可以从文件中读取数据并直接将它们复制到缓冲内存中。 分批顶点属性 通过使用 glVertexAttribPointer我们能够指定顶点数组缓冲内容的属性布局。在顶点数组缓冲中我们对属性进行了交错(Interleave)处理也就是说我们将每一个顶点的位置、法线和/或纹理坐标紧密放置在一起。既然我们现在已经对缓冲有了更多的了解我们可以采取另一种方式 glVertexAttribPointer 原型void glVertexAttribPointer(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const void* pointer); 我们可以做的是将每一种属性类型的向量数据打包(Batch)为一个大的区块而不是对它们进行交错储存。与交错布局123 123 123 123不同我们将采用分批(Batched)的方式1111 2222 3333。 当从文件中加载顶点数据的时候你通常获取到的是一个位置数组、一个法线数组和/或一个纹理坐标数组。我们需要花点力气才能将这些数组转化为一个大的交错数据数组。 使用分批的方式会是更简单的解决方案我们可以很容易使用 glBufferSubData 函数实现 float positions[] { ... }; float normals[] { ... }; float tex[] { ... }; // 填充缓冲 glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(positions), positions); glBufferSubData(GL_ARRAY_BUFFER, sizeof(positions), sizeof(normals), normals); glBufferSubData(GL_ARRAY_BUFFER, sizeof(positions) sizeof(normals), sizeof(tex), tex);这样子我们就能直接将属性数组作为一个整体传递给缓冲而不需要事先处理它们了。我们仍可以将它们合并为一个大的数组再使用 glBufferData 来填充缓冲但对于这种工作使用 glBufferSubData 会更合适一点。 我们还需要更新顶点属性指针来反映这些改变 // 注意stride参数等于顶点属性的大小因为下一个顶点属性向量能在3个或2个分量之后找到。 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), 0); glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)(sizeof(positions))); glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)(sizeof(positions) sizeof(normals)));这给了我们设置顶点属性的另一种方法。使用哪种方法都是可行的它只是设置顶点属性的一种更整洁的方式。但是推荐使用交错方法因为这样一来每个顶点着色器运行时所需要的顶点属性在内存中会更加紧密对齐。 在图形渲染过程中当顶点着色器处理一个顶点时它通常需要访问该顶点的所有属性如位置、颜色、法线等。如果这些属性是交错存储的那么它们在内存中将是连续的这意味着一次内存访问就可以获取到一个顶点所需的所有信息。相反如果是分批存储则可能需要多次内存访问来收集同一个顶点的所有属性这会增加内存延迟并降低性能。 复制缓冲 当你的缓冲已经填充好数据之后你可能会想与其它的缓冲共享其中的数据或者想要将缓冲的内容复制到另一个缓冲当中。glCopyBufferSubData能够让我们相对容易地从一个缓冲中复制数据到另一个缓冲中。这个函数的原型如下 void glCopyBufferSubData(GLenum readtarget, GLenum writetarget, GLintptr readoffset,GLintptr writeoffset, GLsizeiptr size);readtarget 和 writetarget 参数需要填入复制源和复制目标的缓冲目标。比如说我们可以将 VERTEX_ARRAY_BUFFER 缓冲复制到 VERTEX_ELEMENT_ARRAY_BUFFER 缓冲分别将这些缓冲目标设置为读和写的目标。当前绑定到这些缓冲目标的缓冲将会被影响到。 但如果我们想读写数据的两个不同缓冲都为顶点数组缓冲该怎么办呢我们不能同时将两个缓冲绑定到同一个缓冲目标上。正是出于这个原因OpenGL提供给我们另外两个缓冲目标叫做 GL_COPY_READ_BUFFER 和 GL_COPY_WRITE_BUFFER。我们接下来就可以将需要的缓冲绑定到这两个缓冲目标上并将这两个目标作为 readtarget 和 writetarget 参数。 接下来 glCopyBufferSubData 会从 readtarget 中读取 size 大小的数据并将其写入 writetarget 缓冲的 writeoffset 偏移量处。下面这个例子展示了如何复制两个顶点数组缓冲 glBindBuffer(GL_COPY_READ_BUFFER, vbo1); glBindBuffer(GL_COPY_WRITE_BUFFER, vbo2); glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, 0, 0, sizeof(vertexData));我们也可以只将 writetarget 缓冲绑定为新的缓冲目标类型之一 float vertexData[] { ... }; glBindBuffer(GL_ARRAY_BUFFER, vbo1); glBindBuffer(GL_COPY_WRITE_BUFFER, vbo2); glCopyBufferSubData(GL_ARRAY_BUFFER, GL_COPY_WRITE_BUFFER, 0, 0, sizeof(vertexData));有了这些关于如何操作缓冲的额外知识我们已经能够以更有意思的方式使用它们了。当你越深入OpenGL时这些新的缓冲方法将会变得更加有用。在下一节中在我们讨论Uniform缓冲对象(Uniform Buffer Object)时我们将会充分利用 glBufferSubData。 高级GLSL 原文链接高级GLSL - LearnOpenGL CN 这一小节并不会向你展示非常先进非常酷的新特性也不会对场景的视觉质量有显著的提高。但是这一节会或多或少涉及GLSL的一些有趣的地方以及一些很棒的技巧它们可能在今后会帮助到你。简单来说它们就是在组合使用OpenGL和GLSL创建程序时的一些最好要知道的东西和一些会让你生活更加轻松的特性。 我们将会讨论一些有趣的内建变量(Built-in Variable)管理着色器输入和输出的新方式以及一个叫做Uniform缓冲对象(Uniform Buffer Object)的有用工具。 GLSL的内建变量 着色器需要数据才能工作。我们已经学会了使用顶点属性、uniform 和采样器来传递数据。除此之外GLSL 还定义了一些以 gl_ 为前缀的变量它们提供了更多读取/写入数据的方式。我们已经在前面的教程中接触过其中的两个顶点着色器的输出向量 gl_Position 和片段着色器的 gl_FragCoord。 我们将讨论一些有趣的 GLSL 内置输入和输出变量并解释它们如何帮助你。请注意我们不会讨论 GLSL 中存在的所有内置变量。如果你想了解所有内置变量请查看 OpenGL 的 wiki。 顶点着色器变量 gl_Position 我们已经见过 gl_Position 了它是顶点着色器的裁剪空间输出位置向量。如果你想在屏幕上显示任何东西在顶点着色器中设置 gl_Position 是必须的步骤。除此之外它没有其他功能。 gl_PointSize 我们可以使用 GL_POINTS 图元来渲染点。每个顶点都将作为一个单独的点被渲染。我们可以使用 glPointSize 函数来设置点的大小也可以在顶点着色器中修改点的大小 GLSL 定义了一个名为 gl_PointSize 的输出变量它是一个 float 变量用于设置点的宽度与高度以像素为单位。在顶点着色器中修改点的大小可以为每个顶点设置不同的值。 默认情况下在顶点着色器中修改点大小的功能是禁用的。如果需要启用它你需要启用 OpenGL 的 GL_PROGRAM_POINT_SIZE glEnable(GL_PROGRAM_POINT_SIZE);一个简单的例子是将点的大小设置为裁剪空间位置的 z 值也就是顶点与观察者的距离。点的大小会随着观察者与顶点距离的增加而增大。 void main() {gl_Position projection * view * model * vec4(aPos, 1.0); gl_PointSize gl_Position.z; }结果就是当我们远离这些点时它们会变得更大 你可以想到对每个顶点使用不同的点大小会在粒子生成之类的技术中很有意思。 gl_VertexID gl_Position 和 gl_PointSize 都是输出变量因为它们的值作为顶点着色器的输出被读取。我们可以对它们进行写入来改变渲染结果。除此之外顶点着色器还提供了一个有趣的输入变量我们只能读取它它就是 gl_VertexID。 整型变量 gl_VertexID 存储了当前绘制顶点的 ID。当进行索引渲染使用 glDrawElements时这个变量存储了当前绘制顶点的索引值。当不使用索引进行绘制使用 glDrawArrays时这个变量存储了从渲染调用开始已处理的顶点数量。 片段着色器变量 在片段着色器中我们也能访问到一些有趣的变量。GLSL提供给我们两个有趣的输入变量gl_FragCoord 和 gl_FrontFacing gl_FragCoord 我们之前在讨论深度测试时多次提到过 gl_FragCoord因为它的 z 分量等于对应片段的深度值。但是我们也可以使用它的 x 和 y 分量来实现一些有趣的效果。 gl_FragCoord 的 x 和 y 分量是片段的窗口空间Window-space坐标其原点为窗口的左下角。如果我们使用 glViewport 设定了一个 800x600 的窗口那么片段窗口空间坐标的 x 分量将在 0 到 800 之间y 分量在 0 到 600 之间。 通过利用片段着色器我们可以根据片段的窗口坐标计算出不同的颜色。gl_FragCoord 的一个常见用法是用于对比不同片段计算的视觉输出效果这在技术演示中经常可以看到。例如我们可以将屏幕分成两部分在窗口的左侧渲染一种输出在窗口的右侧渲染另一种输出。以下示例片段着色器会根据窗口坐标输出不同的颜色 void main() { if (gl_FragCoord.x 400)FragColor vec4(1.0, 0.0, 0.0, 1.0);elseFragColor vec4(0.0, 1.0, 0.0, 1.0); }由于窗口的宽度是 800当一个像素的 x 坐标小于 400 时它一定在窗口的左侧所以我们给它一个不同的颜色。 我们现在会计算出两个完全不同的片段着色器结果并将它们显示在窗口的两侧。举例来说你可以将它用于测试不同的光照技巧 gl_FrontFacing 片段着色器另一个很有意思的输入变量是 gl_FrontFacing。在面剔除教程中我们提到 OpenGL 能够根据顶点的环绕顺序来决定一个面是正面还是背面。如果我们不启用 GL_FACE_CULL 使用面剔除那么 gl_FrontFacing 将会告诉我们当前片段是属于正面的一部分还是背面的一部分。例如我们可以对正面计算出不同的颜色。 gl_FrontFacing 变量是一个布尔值如果当前片段是正面的一部分则为 true否则为 false。例如我们可以这样创建一个立方体在内部和外部使用不同的纹理 #version 330 core out vec4 FragColor;in vec2 TexCoords;uniform sampler2D frontTexture; uniform sampler2D backTexture;void main() { if (gl_FrontFacing)FragColor texture(frontTexture, TexCoords);elseFragColor texture(backTexture, TexCoords); }如果我们往箱子里面看就能看到使用的是不同的纹理 注意如果你开启了面剔除你就看不到箱子内部的面了所以现在再使用 gl_FrontFacing 就没有意义了。 gl_FragDepth 输入变量 gl_FragCoord 允许我们读取当前片段的窗口空间坐标并获取其深度值。但是它是一个只读Read-only变量。我们无法修改片段的窗口空间坐标但实际上修改片段的深度值是可能的。GLSL 提供了一个名为 gl_FragDepth 的输出变量我们可以使用它在着色器内设置片段的深度值。 要设置深度值我们只需写入一个 0.0 到 1.0 之间的浮点值到输出变量即可 gl_FragDepth 0.0; // 这个片段现在的深度值为 0.0如果着色器没有写入值到 gl_FragDepth它会自动取用 gl_FragCoord.z 的值。 然而由我们自己设置深度值有一个很大的缺点只要我们在片段着色器中对 gl_FragDepth 进行写入OpenGL 就会像深度测试小节中讨论的那样禁用所有的提前深度测试Early Depth Testing。它被禁用的原因是OpenGL 无法在片段着色器运行之前得知片段将拥有的深度值因为片段着色器可能会完全修改这个深度值。 在写入 gl_FragDepth 时你就需要考虑到它所带来的性能影响。然而从 OpenGL 4.2 起我们仍可以对两者进行一定的调和在片段着色器的顶部使用深度条件Depth Condition重新声明 gl_FragDepth 变量 layout (depth_condition) out float gl_FragDepth;condition可以为下面的值 条件描述any默认值。提前深度测试是禁用的你会损失很多性能greater你只能让深度值比gl_FragCoord.z更大less你只能让深度值比gl_FragCoord.z更小unchanged如果你要写入gl_FragDepth你将只能写入gl_FragCoord.z的值 通过将深度条件设置为 greater 或 lessOpenGL 就能假设你只会写入比当前片段深度值更大或更小的值了。这样的话设置深度条件为 greater 使设置的深度值比默认的深度值要大时OpenGL 仍是能够进行提前深度测试的。 下面示例中我们对片段的深度值进行了递增但仍然保留了一些提前深度测试 #version 420 core // 注意 GLSL 的版本 out vec4 FragColor; layout (depth_greater) out float gl_FragDepth;void main() { FragColor vec4(1.0);gl_FragDepth gl_FragCoord.z 0.1; }注意这个特性只在 OpenGL 4.2 或更高版本中可用。 接口块 到目前为止每当我们希望从顶点着色器向片段着色器发送数据时我们都声明了几个对应的输入/输出变量。将它们一个一个声明是着色器间发送数据最简单的方式了但当程序变得更大时你希望发送的可能就不只是几个变量了它还可能包括数组和结构体。 为了帮助我们管理这些变量GLSL 为我们提供了一个叫做 接口块Interface Block 的东西来方便我们组合这些变量。接口块的声明和 struct 的声明有点相像不同的是现在根据它是一个输入还是输出块Block使用 in 或 out 关键字来定义。 在顶点着色器中 #version 330 core layout (location 0) in vec3 aPos; layout (location 1) in vec2 aTexCoords;uniform mat4 model; uniform mat4 view; uniform mat4 projection;out VS_OUT {vec2 TexCoords; } vs_out;void main() {gl_Position projection * view * model * vec4(aPos, 1.0); vs_out.TexCoords aTexCoords; }这次我们声明了一个叫做 vs_out 的接口块它打包了我们希望发送到下一个着色器中的所有输出变量。这只是一个很简单的例子但你可以想象一下它能够帮助你管理着色器的输入和输出。当我们希望将着色器的输入或输出打包为数组时它也会非常有用我们将在下一节讨论几何着色器Geometry Shader时见到。 之后我们还需要在下一个着色器即片段着色器中定义一个输入接口块。块名Block Name 应该是和着色器中一样的VS_OUT但实例名Instance Name顶点着色器中用的是 vs_out可以是随意的但要避免使用误导性的名称比如对实际上包含输入变量的接口块命名为 vs_out。 #version 330 core out vec4 FragColor;in VS_OUT {vec2 TexCoords; } fs_in;uniform sampler2D texture;void main() { FragColor texture(texture, fs_in.TexCoords); }只要两个接口块的名字一样它们对应的输入和输出就会匹配起来。这是帮助你管理代码的又一个有用特性在有几何着色器这样穿插特定着色器阶段的场景下会很有用。 Uniform缓冲对象 我们已经使用 OpenGL 很长时间了学会了一些很酷的技巧但也遇到了一些很麻烦的地方。比如说当使用多于一个的着色器时尽管大部分的 uniform 变量都是相同的我们还是需要不断地设置它们所以为什么要这么麻烦地重复设置它们呢 OpenGL 为我们提供了一个叫做 Uniform 缓冲对象Uniform Buffer Object 的工具它允许我们定义一系列在多个着色器程序中相同的全局 Uniform 变量。当使用 Uniform 缓冲对象的时候我们只需要设置相关的 uniform 一次。当然我们仍需要手动设置每个着色器中不同的 uniform。并且创建和配置 Uniform 缓冲对象会有一点繁琐。 因为 Uniform 缓冲对象仍然是一个缓冲我们可以使用 glGenBuffers 来创建它将它绑定到 GL_UNIFORM_BUFFER 缓冲目标并将所有相关的 uniform 数据存入缓冲。在 Uniform 缓冲对象中储存数据是有一些规则的我们会在之后讨论它。首先我们将使用一个简单的顶点着色器将 projection 和 view 矩阵存储到所谓的 Uniform 块Uniform Block 中 #version 330 core layout (location 0) in vec3 aPos;layout (std140) uniform Matrices {mat4 projection;mat4 view; };uniform mat4 model;void main() {gl_Position projection * view * model * vec4(aPos, 1.0); }在我们大多数的例子中我们都会在每个渲染迭代中对每个着色器设置 projection 和 view Uniform 矩阵。这是利用 Uniform 缓冲对象的一个非常完美的例子因为现在我们只需要存储这些矩阵一次就可以了。 这里我们声明了一个叫做 Matrices 的 Uniform 块它储存了两个 4x4 矩阵。Uniform 块中的变量可以直接访问不需要加块名作为前缀。接下来我们在 OpenGL 代码中将这些矩阵值存入缓冲中每个声明了这个 Uniform 块的着色器都能够访问这些矩阵。 你现在可能会在想 layout (std140) 这个语句是什么意思。它的意思是说当前定义的 Uniform 块对它的内容使用一个特定的内存布局。这个语句设置了 Uniform 块布局Uniform Block Layout。 Uniform块布局 Uniform 块的内容存储在一个缓冲对象中它实际上只是一块预留内存。由于这块内存并不会保存它具体保存的是什么类型的数据我们还需要告诉 OpenGL 内存的哪一部分对应着色器中的哪一个 uniform 变量。 假设着色器中有以下 Uniform 块 layout (std140) uniform ExampleBlock {float value;vec3 vector;mat4 matrix;float values[3];bool boolean;int integer; };我们需要知道的是每个变量的大小以字节为单位和从块起始位置的偏移量以便我们能够按顺序将它们放入缓冲中。每个元素的大小都在 OpenGL 中有清楚地声明并且直接对应于 C 数据类型其中向量和矩阵都是大的浮点数数组。OpenGL 没有声明的是这些变量间的间距Spacing。这允许硬件能够在它认为合适的位置放置变量。例如一些硬件可能会将一个 vec3 放置在 float 边上。不是所有的硬件都能这样处理可能会在附加这个 float 之前先将 vec3 填充Pad为一个 4 个 float 的数组。这个特性本身很棒但是会对我们造成麻烦。 共享布局Shared Layout 默认情况下GLSL 会使用一个叫做共享Shared布局的 Uniform 内存布局共享是因为一旦硬件定义了偏移量它们在多个程序中是共享且一致的。使用共享布局时GLSL 可以为了优化而对 uniform 变量的位置进行变动只要变量的顺序保持不变。 由于我们无法知道每个 uniform 变量的偏移量我们也就不知道如何准确地填充我们的 Uniform 缓冲了。我们能够使用像 glGetUniformIndices 这样的函数来查询这个信息但这超出了本节的范围。 std140 布局 虽然共享布局给了我们很多节省空间的优化但是我们需要查询每个 uniform 变量的偏移量这会产生非常多的工作量。通常的做法是不使用共享布局而是使用 std140 布局。 std140 布局声明了每个变量的偏移量都是由一系列规则所决定的这显式地声明了每个变量类型的内存布局。由于这是显式提及的我们可以手动计算出每个变量的偏移量。 每个变量都有一个基准对齐量Base Alignment它等于一个变量在 Uniform 块中所占据的空间包括填充量Padding这个基准对齐量是使用 std140 布局的规则计算出来的。 接下来对每个变量我们再计算它的对齐偏移量Aligned Offset它是一个变量从块起始位置的字节偏移量。一个变量的对齐字节偏移量必须等于基准对齐量的倍数。 布局规则的原文在OpenGL的Uniform缓冲规范这里找到但我们会在下面列出最常见的规则。GLSL 中的每个变量例如 int、float 和 bool都被定义为 4 字节量。每 4 个字节将用一个 N 来表示。 类型布局规则标量比如int和bool每个标量的基准对齐量为N。向量2N或者4N。这意味着vec3的基准对齐量为4N。标量或向量的数组每个元素的基准对齐量与vec4的相同。矩阵储存为列向量的数组每个向量的基准对齐量与vec4的相同。结构体等于所有元素根据规则计算后的大小但会填充到vec4大小的倍数。 和 OpenGL 大多数的规范一样使用例子就能更容易地理解。我们会使用之前引入的那个叫做 ExampleBlock 的 Uniform 块并使用 std140 布局计算出每个成员的对齐偏移量 layout (std140) uniform ExampleBlock {// 基准对齐量 // 对齐偏移量float value; // 4 // 0 vec3 vector; // 16 // 16 (必须是 16 的倍数所以 4-16)mat4 matrix; // 16 // 32 (列 0)// 16 // 48 (列 1)// 16 // 64 (列 2)// 16 // 80 (列 3)float values[3]; // 16 // 96 (values[0])// 16 // 112 (values[1])// 16 // 128 (values[2])bool boolean; // 4 // 144int integer; // 4 // 148 };作为练习尝试去自己计算一下偏移量并和表格进行对比。使用计算后的偏移量值根据 std140 布局的规则我们就能使用像 glBufferSubData 的函数将变量数据按照偏移量填充进缓冲中了。虽然 std140 布局不是最高效的布局但它保证了内存布局在每个声明了这个 Uniform 块的程序中是一致的。 注意对于数组 values[3] 的每一个元素需要单独设置虽然一个 float 只占 4 个字节但是数组元素的基准对齐量与 vec4 保持一致为 16 个字节。 unsigned int ubo; glGenBuffers(1, ubo); glBindBuffer(GL_UNIFORM_BUFFER, ubo); glBufferData(GL_UNIFORM_BUFFER, 152, NULL, GL_STATIC_DRAW);glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(float), value); glBufferSubData(GL_UNIFORM_BUFFER, 16, sizeof(glm::vec3), vector); glBufferSubData(GL_UNIFORM_BUFFER, 32, sizeof(glm::mat4), matrix); for (int i 0; i 3; i) {// 计算当前元素的实际偏移量// 基础偏移量96加上每个元素16字节的间隔*iglBufferSubData(GL_UNIFORM_BUFFER, 96 i * 16, sizeof(float), values[i]); } glBufferSubData(GL_UNIFORM_BUFFER, 144, sizeof(bool), boolean); glBufferSubData(GL_UNIFORM_BUFFER, 148, sizeof(int), integer); glBindBuffer(GL_UNIFORM_BUFFER, 0);通过在 Uniform 块定义之前添加 layout (std140) 语句我们告诉 OpenGL 这个 Uniform 块使用的是 std140 布局。除此之外还可以选择两个布局但它们都需要我们在填充缓冲之前先查询每个偏移量。我们已经见过 shared 布局了剩下的一个布局是 packed。当使用紧凑Packed布局时是不能保证这个布局在每个程序中保持不变的即非共享因为它允许编译器去将 uniform 变量从 Uniform 块中优化掉这在每个着色器中都可能是不同的。 接下来展示使用 glGetUniformIndices 与 glGetActiveUniformsiv 动态查询偏移位置 // 获取uniform变量的索引 glGetUniformIndices(GLuint program, GLsizei uniformCount, const GLchar *const*uniformNames, GLuint *uniformIndices); // 查询每个uniform变量的偏移量 glGetActiveUniformsiv(GLuint program, GLsizei uniformCount, const GLuint *uniformIndices, GLenum pname, GLint *params)由于我们只能查询数组首元素的对齐偏移量针对于 shared 与 packed 分布我们需要推导每一个元素占了多少字节基准对齐量然后计算每一个元素的对齐偏移量。但这种做法会存在潜在风险packed 旨在尽可能紧凑地存储uniform变量而不遵循固定的对齐规则所以在某些情况下推导会出现错误。 const char* uniformNames[] { value, vector, matrix, values,boolean, integer }; GLuint indices[6]; // 获取uniform变量的索引 glGetUniformIndices(ourShader.ID, 6, uniformNames, indices);// 查询每个uniform变量的偏移量 int offsets[6]; glGetActiveUniformsiv(ourShader.ID, 6, indices, GL_UNIFORM_OFFSET, offsets);glBufferSubData(GL_UNIFORM_BUFFER, offsets[0], sizeof(float), value); glBufferSubData(GL_UNIFORM_BUFFER, offsets[1], sizeof(glm::vec3), vector); glBufferSubData(GL_UNIFORM_BUFFER, offsets[2], sizeof(glm::mat4), matrix); // shared或packed的推导 for (int i 0; i 3; i) {glBufferSubData(GL_UNIFORM_BUFFER, offsets[3] sizeof(float)*i, sizeof(float), values[i]); } glBufferSubData(GL_UNIFORM_BUFFER, offsets[4], sizeof(int), boolean); glBufferSubData(GL_UNIFORM_BUFFER, offsets[5], sizeof(int), integer); glBindBuffer(GL_UNIFORM_BUFFER, 0);同时需要注意shared 布局声明为 layout (shared) uniform ExampleBlock但若你希望为 packed 布局不需要加 layout直接 uniform ExampleBlock 使用Uniform缓冲 我们已经讨论了如何在着色器中定义 Uniform 块并设定它们的内存布局了但我们还没有讨论该如何使用它们。 首先我们需要调用 glGenBuffers创建一个 Uniform 缓冲对象。一旦我们有了一个缓冲对象我们需要将它绑定到 GL_UNIFORM_BUFFER 目标并调用 glBufferData分配足够的内存。 unsigned int uboExampleBlock; glGenBuffers(1, uboExampleBlock); glBindBuffer(GL_UNIFORM_BUFFER, uboExampleBlock); glBufferData(GL_UNIFORM_BUFFER, 152, NULL, GL_STATIC_DRAW); // 分配 152 字节的内存 glBindBuffer(GL_UNIFORM_BUFFER, 0);现在每当我们需要对缓冲更新或者插入数据我们都会绑定到 uboExampleBlock并使用 glBufferSubData 来更新它的内存。我们只需要更新这个 Uniform 缓冲一次所有使用这个缓冲的着色器就都使用的是更新后的数据了。但是如何才能让 OpenGL 知道哪个 Uniform 缓冲对应的是哪个 Uniform 块呢 在 OpenGL 上下文中定义了一些绑定点Binding Point我们可以将一个 Uniform 缓冲链接至它。在创建 Uniform 缓冲之后我们将它绑定到其中一个绑定点上并将着色器中的 Uniform 块绑定到相同的绑定点把它们连接到一起。下面的图示展示了这个 你可以看到我们可以绑定多个 Uniform 缓冲到不同的绑定点上。因为着色器 A 和着色器 B 都有一个链接到绑定点 0 的 Uniform 块它们的 Uniform 块将会共享相同的 uniform 数据uboMatrices前提条件是两个着色器都定义了相同的 Matrices Uniform 块。 为了将 Uniform 块绑定到一个特定的绑定点中我们需要调用 glUniformBlockBinding 函数它的第一个参数是一个程序对象之后是一个 Uniform 块索引和链接到的绑定点。Uniform 块索引Uniform Block Index是着色器中已定义 Uniform 块的位置值索引。这可以通过调用 glGetUniformBlockIndex 来获取它接受一个程序对象和 Uniform 块的名称。 glGetUniformBlockIndex(GLuint program, const GLchar *uniformBlockName); glUniformBlockBinding(GLuint program, GLuint uniformBlockIndex, GLuint uniformBlockBinding);我们可以用以下方式将图示中的 Lights Uniform 块链接到绑定点 2 unsigned int lights_index glGetUniformBlockIndex(shaderA.ID, Lights); glUniformBlockBinding(shaderA.ID, lights_index, 2);注意我们需要对每个着色器重复这一步骤。 从 OpenGL 4.2 版本起你也可以添加一个布局标识符显式地将 Uniform 块的绑定点储存在着色器中这样就不用再调用 glGetUniformBlockIndex 和 glUniformBlockBinding 了。下面的代码显式地设置了 Lights Uniform 块的绑定点。 layout(std140, binding 2) uniform Lights { ... };接下来我们还需要绑定 Uniform 缓冲对象到相同的绑定点上这可以使用 glBindBufferBase 或 glBindBufferRange 来完成。 glBindBufferBase(GLenum target, GLuint index, GLuint buffer); // offset必须是 GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT 的倍数原因见下 glBindBufferRange(GLenum target, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size);glBindBufferBase(GL_UNIFORM_BUFFER, 2, uboExampleBlock); // 或 glBindBufferRange(GL_UNIFORM_BUFFER, 2, uboExampleBlock, 0, 152);glBindbufferBase 需要一个目标、一个绑定点索引和一个 Uniform 缓冲对象作为它的参数。这个函数将 uboExampleBlock 链接到绑定点 2 上自此绑定点的两端都链接上了。你也可以使用 glBindBufferRange 函数它需要一个附加的偏移量和大小参数这样子你可以绑定 Uniform 缓冲的特定一部分到绑定点中。通过使用 glBindBufferRange 函数你可以让多个不同的 Uniform 块绑定到同一个 Uniform 缓冲对象上。 现在所有的东西都配置完毕了我们可以开始向 Uniform 缓冲中添加数据了。只要我们需要就可以使用 glBufferSubData 函数用一个字节数组添加所有的数据或者更新缓冲的一部分。要想更新 uniform 变量 boolean我们可以用以下方式更新 Uniform 缓冲对象 glBindBuffer(GL_UNIFORM_BUFFER, uboExampleBlock); int b true; // GLSL 中的 bool 是 4 字节的所以我们将它存为一个 integer glBufferSubData(GL_UNIFORM_BUFFER, 144, 4, b); glBindBuffer(GL_UNIFORM_BUFFER, 0);同样的步骤也能应用到 Uniform 块中其他的 uniform 变量上但需要使用不同的范围参数。 评论区 jim jim指出了一个问题 我测试了一下确实如此mark一下。测试用的代码如下 unsigned int ubo; glGenBuffers(1, ubo); glBindBuffer(GL_UNIFORM_BUFFER, ubo); glBufferData(GL_UNIFORM_BUFFER, 512, NULL, GL_STATIC_DRAW); float value 1.0f; glm::vec3 vector(0.0f, 1.0f, 0.0f); glm::mat4 matrix; matrix[0] glm::vec4(1.0f, 0.0f, 0.0f, 0.0f); matrix[1] glm::vec4(0.0f, 1.0f, 0.0f, 0.0f); matrix[2] glm::vec4(0.0f, 0.0f, 1.0f, 0.0f); matrix[3] glm::vec4(0.0f, 0.0f, 0.0f, 1.0f); float values[3] { 1.0f, 2.0f, 3.0f }; bool boolean true; int integer 1;const char* uniformNames[] { value, vector, matrix, values,boolean, integer }; GLuint indices[6]; // 获取uniform变量的索引 glGetUniformIndices(ourShader.ID, 6, uniformNames, indices);// 查询每个uniform变量的偏移量 int offsets[6]; glGetActiveUniformsiv(ourShader.ID, 6, indices, GL_UNIFORM_OFFSET, offsets);for (int i 0; i 6; i) {printf(Element %d of array values has offset: %d\n, i, offsets[i]); }GLint uniformBufferOffsetAlign 256; //glGetIntegerv(GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT, uniformBufferOffsetAlign); std::cout Uniform buffer offset alignment: uniformBufferOffsetAlign std::endl; glBufferSubData(GL_UNIFORM_BUFFER, offsets[0] uniformBufferOffsetAlign, sizeof(float), value); glBufferSubData(GL_UNIFORM_BUFFER, offsets[1] uniformBufferOffsetAlign, sizeof(glm::vec3), vector); glBufferSubData(GL_UNIFORM_BUFFER, offsets[2] uniformBufferOffsetAlign, sizeof(glm::mat4), matrix); // 假设均匀分布 int perOffset (offsets[4] - offsets[3]) / 3; for (int i 0; i 3; i) {glBufferSubData(GL_UNIFORM_BUFFER, perOffset * i offsets[3] uniformBufferOffsetAlign, sizeof(float), values[i]); }glBufferSubData(GL_UNIFORM_BUFFER, offsets[4] uniformBufferOffsetAlign, sizeof(int), boolean); glBufferSubData(GL_UNIFORM_BUFFER, offsets[5] uniformBufferOffsetAlign, sizeof(int), integer); glBindBuffer(GL_UNIFORM_BUFFER, 0);GLuint blockIndex glGetUniformBlockIndex(ourShader.ID, ExampleBlock); glUniformBlockBinding(ourShader.ID, blockIndex, 0); glBindBufferRange(GL_UNIFORM_BUFFER, 0, ubo, uniformBufferOffsetAlign, 152);一个简单的例子 我们来展示一个真正使用 Uniform 缓冲对象的例子。如果我们回头看看之前所有的代码例子我们会不断地使用 3 个矩阵投影、观察和模型矩阵。在所有的这些矩阵中只有模型矩阵会频繁变动。如果我们有多个着色器使用了这同一组矩阵那么使用 Uniform 缓冲对象可能会更好。 我们会将投影和模型矩阵存储到一个叫做 Matrices 的 Uniform 块中。我们不会将模型矩阵存在这里因为模型矩阵在不同的着色器中会不断改变所以使用 Uniform 缓冲对象并不会带来什么好处。 #version 330 core layout (location 0) in vec3 aPos;layout (std140) uniform Matrices {mat4 projection;mat4 view; };uniform mat4 model;void main() {gl_Position projection * view * model * vec4(aPos, 1.0); }这里没什么特别的除了我们现在使用的是一个 std140 布局的 Uniform 块。我们将在例子程序中显示 4 个立方体每个立方体都是使用不同的着色器程序渲染的。这 4 个着色器程序将使用相同的顶点着色器但使用的是不同的片段着色器每个着色器会输出不同的颜色。 首先我们将顶点着色器的 Uniform 块设置为绑定点 0。注意我们需要对每个着色器都设置一遍。 unsigned int uniformBlockIndexRed glGetUniformBlockIndex(shaderRed.ID, Matrices); unsigned int uniformBlockIndexGreen glGetUniformBlockIndex(shaderGreen.ID, Matrices); unsigned int uniformBlockIndexBlue glGetUniformBlockIndex(shaderBlue.ID, Matrices); unsigned int uniformBlockIndexYellow glGetUniformBlockIndex(shaderYellow.ID, Matrices); glUniformBlockBinding(shaderRed.ID, uniformBlockIndexRed, 0); glUniformBlockBinding(shaderGreen.ID, uniformBlockIndexGreen, 0); glUniformBlockBinding(shaderBlue.ID, uniformBlockIndexBlue, 0); glUniformBlockBinding(shaderYellow.ID, uniformBlockIndexYellow, 0);接下来我们创建 Uniform 缓冲对象本身并将其绑定到绑定点 0 unsigned int uboMatrices; glGenBuffers(1, uboMatrices);glBindBuffer(GL_UNIFORM_BUFFER, uboMatrices); glBufferData(GL_UNIFORM_BUFFER, 2 * sizeof(glm::mat4), NULL, GL_STATIC_DRAW); glBindBuffer(GL_UNIFORM_BUFFER, 0);glBindBufferRange(GL_UNIFORM_BUFFER, 0, uboMatrices, 0, 2 * sizeof(glm::mat4));首先我们为缓冲分配了足够的内存它等于 glm::mat4 大小的两倍。GLM 矩阵类型的大小直接对应于 GLSL 中的 mat4。接下来我们将缓冲中的特定范围在这里是整个缓冲链接到绑定点 0。 剩余的就是填充这个缓冲了。如果我们将投影矩阵的视野Field of View值保持不变所以摄像机就没有缩放了我们只需要将其在程序中定义一次——这也意味着我们只需要将它插入到缓冲中一次。因为我们已经为缓冲对象分配了足够的内存我们可以使用 glBufferSubData 在进入渲染循环之前存储投影矩阵 glm::mat4 projection glm::perspective(glm::radians(45.0f), (float)width/(float)height, 0.1f, 100.0f); glBindBuffer(GL_UNIFORM_BUFFER, uboMatrices); glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(glm::mat4), glm::value_ptr(projection)); glBindBuffer(GL_UNIFORM_BUFFER, 0);这里我们将投影矩阵储存在 Uniform 缓冲的前半部分。在每次渲染迭代中绘制物体之前我们会将观察矩阵更新到缓冲的后半部分 glm::mat4 view camera.GetViewMatrix(); glBindBuffer(GL_UNIFORM_BUFFER, uboMatrices); glBufferSubData(GL_UNIFORM_BUFFER, sizeof(glm::mat4), sizeof(glm::mat4), glm::value_ptr(view)); glBindBuffer(GL_UNIFORM_BUFFER, 0);Uniform 缓冲对象的部分就结束了。每个包含了 Matrices 这个 Uniform 块的顶点着色器将会包含储存在 uboMatrices 中的数据。所以如果我们现在要用 4 个不同的着色器绘制 4 个立方体它们的投影和观察矩阵都会是一样的。 glBindVertexArray(cubeVAO); shaderRed.use(); glm::mat4 model; model glm::translate(model, glm::vec3(-0.75f, 0.75f, 0.0f)); // 移动到左上角 shaderRed.setMat4(model, model); glDrawArrays(GL_TRIANGLES, 0, 36); // ... 绘制绿色立方体 // ... 绘制蓝色立方体 // ... 绘制黄色立方体 唯一需要设置的 uniform 只剩 model uniform 了。在像这样的场景中使用 Uniform 缓冲对象会让我们在每个着色器中都剩下一些 uniform 调用。最终的结果会是这样的 由于修改了模型矩阵每个立方体都移动到了窗口的一边并且由于使用了不同的片段着色器它们的颜色也不同。这只是一个很简单的情景我们可能需要使用 Uniform 缓冲对象但任何大型的渲染程序都可能同时激活上百个着色器程序这时 Uniform 缓冲对象的优势就会很大地体现出来了。 你可以在这里找到uniform例子程序的完整源代码。 Uniform 缓冲对象比起独立的 uniform 有很多好处。 性能优势 一次设置很多 uniform 会比一个一个设置多个 uniform 要快很多。易于管理 比起在多个着色器中修改同样的 uniform在 Uniform 缓冲中修改一次会更容易一些。更大的 Uniform 容量 如果使用 Uniform 缓冲对象的话你可以在着色器中使用更多的 uniform。OpenGL 限制了它能够处理的 uniform 数量这可以通过 GL_MAX_VERTEX_UNIFORM_COMPONENTS 来查询。当使用 Uniform 缓冲对象时最大的数量会更高。所以当你达到了 uniform 的最大数量时比如再做骨骼动画Skeletal Animation的时候你总是可以选择使用 Uniform 缓冲对象。 本次项目源码高级GLSL - GitCode 几何着色器 原文链接几何着色器 - LearnOpenGL CN 在顶点和片段着色器之间有一个可选的几何着色器Geometry Shader。几何着色器的输入是一个图元如点或三角形的一组顶点。几何着色器可以在顶点发送到下一着色器阶段之前对它们进行任意变换。然而几何着色器最有趣的地方在于它能够将这一组顶点变换为完全不同的图元并且还能生成比原来更多的顶点。 废话不多说我们直接先看一个几何着色器的例子 #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(); }在几何着色器的顶部我们需要声明从顶点着色器输入的图元类型。这需要在 in 关键字前声明一个布局修饰符Layout Qualifier。这个输入布局修饰符可以从顶点着色器接收下列任何一个图元值 points绘制 GL_POINTS 图元时1。lines绘制 GL_LINES 或 GL_LINE_STRIP 时2。lines_adjacencyGL_LINES_ADJACENCY 或 GL_LINE_STRIP_ADJACENCY4。trianglesGL_TRIANGLES、GL_TRIANGLE_STRIP 或 GL_TRIANGLE_FAN3。triangles_adjacencyGL_TRIANGLES_ADJACENCY 或 GL_TRIANGLE_STRIP_ADJACENCY6。 以上是能提供给 glDrawArrays 渲染函数的几乎所有图元了。如果我们想要将顶点绘制为 GL_TRIANGLES我们就需要将输入修饰符设置为 triangles。括号内的数字表示的是一个图元所包含的最小顶点数。 接下来我们还需要指定几何着色器输出的图元类型这需要在 out 关键字前面加一个布局修饰符。和输入布局修饰符一样输出布局修饰符也可以接受几个图元值 pointsline_striptriangle_strip 有了这 3 个输出修饰符我们就可以使用输入图元创建几乎任意的形状了。要生成一个三角形的话我们将输出定义为 triangle_strip并输出 3 个顶点。 几何着色器同时希望我们设置一个它最大能够输出的顶点数量如果你超过了这个值OpenGL 将不会绘制多出的顶点这个也可以在 out 关键字的布局修饰符中设置。在这个例子中我们将输出一个 line_strip并将最大顶点数设置为 2 个。 如果你不知道什么是线条Line Strip线条连接了一组点形成一条连续的线它最少要由两个点来组成。在渲染函数中每多加一个点就会在这个点与前一个点之间形成一条新的线。在下面这张图中我们有 5 个顶点 为了生成更有意义的结果我们需要某种方式来获取前一着色器阶段的输出。GLSL 提供给我们一个名为 gl_in 的内置Built-in变量在内部看起来可能是这样的 in gl_Vertex {vec4 gl_Position;float gl_PointSize;float gl_ClipDistance[]; } gl_in[];这里它被声明为一个接口块Interface Block我们在上一节已经讨论过它包含了几个很有意思的变量其中最有趣的一个是 gl_Position它是和顶点着色器输出非常相似的一个向量。 要注意的是它被声明为一个数组因为大多数的渲染图元包含多于 1 个的顶点而几何着色器的输入是一个图元的所有顶点。 有了之前顶点着色器阶段的顶点数据我们就可以使用 2 个几何着色器函数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(); }每次我们调用 EmitVertex 时gl_Position 中的向量会被添加到图元中来。当 EndPrimitive 被调用时所有发射出的Emitted顶点都会合成为指定的输出渲染图元。在一个或多个 EmitVertex 调用之后重复调用 EndPrimitive 能够生成多个图元。在这个例子中我们发射了两个顶点它们从原始顶点位置平移了一段距离之后调用了 EndPrimitive将这两个顶点合成为一个包含两个顶点的线条。 现在你大概了解了几何着色器的工作方式你可能已经猜出这个几何着色器是做什么的了。它接受一个点图元作为输入以这个点为中心创建一条水平的线图元。如果我们渲染它看起来会是这样的 目前还并没有什么令人惊叹的效果但考虑到这个输出是通过调用下面的渲染函数来生成的它还是很有意思的 glDrawArrays(GL_POINTS, 0, 4);虽然这是一个比较简单的例子它的确向你展示了如何能够使用几何着色器来动态地生成新的形状。在之后我们会利用几何着色器创建出更有意思的效果但现在我们仍将从创建一个简单的几何着色器开始。 使用几何着色器 为了展示几何着色器的用法我们将会渲染一个非常简单的场景我们只会在标准化设备坐标的 z 平面上绘制四个点。这些点的坐标是 float points[] {-0.5f, 0.5f, // 左上0.5f, 0.5f, // 右上0.5f, -0.5f, // 右下-0.5f, -0.5f // 左下 };顶点着色器只需要在 z 平面绘制点就可以了所以我们将使用一个最基本的顶点着色器 #version 330 core layout (location 0) in vec2 aPos;void main() {gl_Position vec4(aPos.x, aPos.y, 0.0, 1.0); }直接在片段着色器中硬编码将所有的点都输出为绿色 #version 330 core out vec4 FragColor;void main() {FragColor vec4(0.0, 1.0, 0.0, 1.0); }为点的顶点数据生成一个 VAO 和一个 VBO然后使用 glDrawArrays 进行绘制 shader.use(); glBindVertexArray(VAO); glDrawArrays(GL_POINTS, 0, 4);结果是在黑暗的场景中有四个很难看见的绿点 但我们之前不是学过这些吗是的但是现在我们将会添加一个几何着色器为场景添加活力。 出于学习目的我们将会创建一个传递Pass-through几何着色器它会接收一个点图元并直接将它传递Pass到下一个着色器 #version 330 core layout (points) in; layout (points, max_vertices 1) out;void main() { gl_Position gl_in[0].gl_Position; EmitVertex();EndPrimitive(); }现在这个几何着色器应该很容易理解了它只是将它接收到的顶点位置不作修改直接发射出去并生成一个点图元。 和顶点与片段着色器一样几何着色器也需要编译和链接但这次在创建着色器时我们将会使用 GL_GEOMETRY_SHADER 作为着色器类型 geometryShader glCreateShader(GL_GEOMETRY_SHADER); glShaderSource(geometryShader, 1, gShaderCode, NULL); glCompileShader(geometryShader); ... glAttachShader(program, geometryShader); glLinkProgram(program);着色器编译的代码和顶点与片段着色器代码都是一样的。记得要检查编译和链接错误 修改 shader 类的构造函数以适应几何着色器的编译与链接 Shader::Shader(const char* vertexPath, const char* fragmentPath, const char* geometryPath) {// 1. 从文件路径中获取顶点/片段着色器std::string vertexCode;std::string fragmentCode;std::string geometryCode;std::ifstream vShaderFile;std::ifstream fShaderFile;std::ifstream gShaderFile;// 保证ifstream对象可以抛出异常vShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);fShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);gShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);try{// 打开文件vShaderFile.open(vertexPath);fShaderFile.open(fragmentPath);std::stringstream vShaderStream, fShaderStream;// 读取文件的缓冲内容到数据流中vShaderStream vShaderFile.rdbuf();fShaderStream fShaderFile.rdbuf();// 关闭文件处理器vShaderFile.close();fShaderFile.close();// 转换数据流到stringvertexCode vShaderStream.str();fragmentCode fShaderStream.str();if (geometryPath ! nullptr){gShaderFile.open(geometryPath);std::stringstream gShaderStream;gShaderStream gShaderFile.rdbuf();gShaderFile.close();geometryCode gShaderStream.str();}}catch (std::ifstream::failure e){std::cout ERROR::SHADER::FILE_NOT_SUCCESFULLY_READ std::endl;}const char* vShaderCode vertexCode.c_str();const char* fShaderCode fragmentCode.c_str();// 2. 编译着色器unsigned int vertex, fragment, geometry;int success;char infoLog[512];// 顶点着色器vertex glCreateShader(GL_VERTEX_SHADER);glShaderSource(vertex, 1, vShaderCode, NULL);glCompileShader(vertex);// 打印编译错误如果有的话glGetShaderiv(vertex, GL_COMPILE_STATUS, success);if (!success){glGetShaderInfoLog(vertex, 512, NULL, infoLog);std::cout ERROR::SHADER::VERTEX::COMPILATION_FAILED\n infoLog std::endl;};// 片段着色器fragment glCreateShader(GL_FRAGMENT_SHADER);glShaderSource(fragment, 1, fShaderCode, NULL);glCompileShader(fragment);// 打印编译错误如果有的话glGetShaderiv(fragment, GL_COMPILE_STATUS, success);if (!success){glGetShaderInfoLog(fragment, 512, NULL, infoLog);std::cout ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n infoLog std::endl;}// 着色器程序ID glCreateProgram();glAttachShader(ID, vertex);glAttachShader(ID, fragment);// 顶点着色器if (geometryPath ! nullptr){const char* gShaderCode geometryCode.c_str();geometry glCreateShader(GL_GEOMETRY_SHADER);glShaderSource(geometry, 1, gShaderCode, NULL);glCompileShader(geometry);glGetShaderiv(geometry, GL_COMPILE_STATUS, success);if (!success){glGetShaderInfoLog(geometry, 512, NULL, infoLog);std::cout ERROR::SHADER::GEOMETRY::COMPILATION_FAILED\n infoLog std::endl;}glAttachShader(ID, geometry);}glLinkProgram(ID);// 打印连接错误如果有的话glGetProgramiv(ID, GL_LINK_STATUS, success);if (!success){glGetProgramInfoLog(ID, 512, NULL, infoLog);std::cout ERROR::SHADER::PROGRAM::LINKING_FAILED\n infoLog std::endl;}// 删除着色器它们已经链接到我们的程序中了已经不再需要了glDeleteShader(vertex);glDeleteShader(fragment);if (geometryPath ! nullptr){glDeleteShader(geometry);} }如果你现在编译并运行程序会看到和下面类似的结果 这和没使用几何着色器时是完全一样的我承认这是有点无聊但既然我们仍然能够绘制这些点所以几何着色器是正常工作的现在是时候做点更有趣的东西了 造几个房子 绘制点和线并没有那么有趣所以我们会使用一点创造力利用几何着色器在每个点的位置上绘制一个房子。要实现这个我们可以将几何着色器的输出设置为 triangle_strip并绘制三个三角形其中两个组成一个正方形另一个用作房顶。 在 OpenGL 中三角形带Triangle Strip是绘制三角形更高效的方式它使用顶点更少。在第一个三角形绘制完之后每个后续顶点将会在上一个三角形边上生成另一个三角形每 3 个临近的顶点将会形成一个三角形。如果我们一共有 6 个构成三角形带的顶点那么我们会得到这些三角形(1, 2, 3)、(2, 3, 4)、(3, 4, 5) 和 (4, 5, 6)共形成 4 个三角形。一个三角形带至少需要 3 个顶点并会生成 N-2 个三角形。使用 6 个顶点我们创建了 6-2 4 个三角形。下面这幅图展示了这点 通过使用三角形带作为几何着色器的输出我们可以很容易创建出需要的房子形状只需要以正确的顺序生成 3 个相连的三角形就行了。下面这幅图展示了顶点绘制的顺序蓝点代表的是输入点 几何着色器 #version 330 core layout (points) in; layout (triangle_strip, max_vertices 5) out;void build_house(vec4 position) { 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 个顶点每个顶点都是原始点的位置加上一个偏移量来组成一个大的三角形带。最终的图元会被光栅化然后片段着色器会处理整个三角形带最终在每个绘制的点处生成一个绿色房子 你可以看到每个房子实际上是由 3 个三角形组成——但是都由空间中一点变换从而绘制。这些绿房子看起来是有点无聊所以我们会再给每个房子分配一个不同的颜色。为了实现这个我们需要在顶点着色器中添加一个额外的顶点属性表示颜色信息将它传递至几何着色器并再次发送到片段着色器中。 下面是更新后的顶点数据 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 // 左下 };然后我们更新顶点着色器使用一个接口块将颜色属性发送到几何着色器中 in VS_OUT {vec3 color; } gs_in[];因为几何着色器是作用于输入的一组顶点的从顶点着色器发来输入数据总是会以数组的形式表示出来即便我们现在只有一个顶点。 我们并不是必须要用接口块来向几何着色器传递数据。我们也可以这样写 in vec3 vColor[];如果顶点着色器发送的颜色向量是 out vec3 vColor那这么写就没问题。然而接口块在几何着色器这样的着色器中会更容易处理一点。实际上几何着色器的输入能够变得非常大将它们合并为一个大的接口块数组会更符合逻辑一点。 接下来我们需要在片段着色器阶段声明一个输出颜色向量 out vec3 fColor;由于片段着色器只需要一个经过插值的颜色因此发送多个颜色是没有意义的。所以fColor 向量不是一个数组而是一个单独的向量。当发射一个顶点时每个顶点将使用最后存储在 fColor 中的值该值将用于片段着色器的运行。对于我们的房子我们只需要在发射第一个顶点之前使用顶点着色器中的颜色填充 fColor 一次即可。 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();所有发射的顶点都将包含最后存储在 fColor 中的值即顶点的颜色属性值。因此所有房子都将具有它们自己的颜色 仅仅是为了有趣我们也可以假装这是冬天将最后一个顶点的颜色设置为白色给屋顶落上一些雪颜色插值。 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();最终结果看起来是这样的 你可以将你的代码与这里的 OpenGL 代码进行比对。 有了几何着色器即使是最简单的图元也能呈现出丰富的视觉效果。这些形状在 GPU 的高速硬件中动态生成相比于在顶点缓冲中手动定义模型效率更高。因此几何着色器是简单且重复形状的理想优化工具例如体素世界中的方块以及户外场景中的植被。 爆破物体 绘制房子很有趣但我们不会经常这样做。接下来我们将深入探讨如何使用几何着色器来实现物体爆破效果。虽然这种效果不常用但它能充分展示几何着色器的强大功能。 当我们说“爆破”一个物体时并不是真的要炸毁顶点数据而是指将每个三角形沿其法向量方向移动一段距离。这样整个物体看起来就像沿着每个三角形的法线向量“爆炸”一样。这种效果在纳米模型的渲染中尤为常见看起来就像这样 注意若是出现纹理混乱请检测 Model:: loadModel 是否需要翻转 UV 这种几何着色器效果的一个优点是无论物体的复杂程度如何它都可以应用。 由于我们想沿着三角形的法向量位移每个顶点因此首先需要计算该法向量。我们要做的就是计算垂直于三角形表面的向量仅使用我们可以访问的 3 个顶点。你可能还记得在变换小节中我们使用叉乘来获得垂直于其他两个向量的向量。如果我们能获得两个平行于三角形表面的向量 a 和 b就可以对这两个向量进行叉乘来获得法向量。以下几何着色器函数正是这样做的它使用 3 个输入顶点坐标来获得法向量 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)); }这里我们使用减法获得了两个平行于三角形表面的向量 a 和 b。由于两个向量相减可以得到这两个向量之间的差并且三个点都位于三角平面上因此对任意两个向量相减都可以得到一个平行于平面的向量。注意如果我们交换了 cross 函数中 a 和 b 的位置我们将得到一个指向相反方向的法向量——这里的顺序很重要 既然知道了如何计算法向量我们就可以创建一个 explode 函数了它使用法向量和顶点位置向量作为参数。此函数将返回一个新向量它是位置向量沿法线向量位移后的结果 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); }函数本身应该不复杂。sin 函数接收一个 time 参数它根据时间返回一个 -1.0 到 1.0 之间的值。因为我们不想让物体向内“爆炸”Implode所以我们将 sin 值变换到 [0, 1] 的范围内。最终结果会乘以 normal 向量并且最终的 direction 向量会添加到位置向量上。 当使用我们的模型加载器绘制模型时爆炸效果的完整几何着色器如下所示 #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(); }注意我们在发射顶点之前输出了相应的纹理坐标。 同时别忘了在 OpenGL 代码中设置 time 变量 shader.setFloat(time, glfwGetTime());最终效果是3D 模型看起来随着时间不断地“爆炸”其顶点然后再恢复正常状态。虽然这不是很实用但它确实展示了几何着色器更高级的用法。你可以将你的代码与这里的完整源代码进行比较。 摘自评论区 CasonCaly 需要注意的是几何着色器中的 gl_Position 是从顶点着色器中传过来的。也就是说顶点着色器中算出来的顶点是什么空间的那么几何着色器中就是哪个空间的。 作者给的两个例子中爆炸的例子是基于投影空间的毛发的例子是基于观察空间的。 在爆炸的例子中作者应该是偷懒了直接用投影空间的顶点去算法线。从效果来看是没有问题的但如果你拿一个立方体的箱子做试验你就非常容易发现问题。正确且简单的做法是直接把原始顶点传到几何着色器中。 出错误的原因为在投影空间用顶点计算法线然后偏移顶点坐标。那为什么不能在投影空间计算法线呢你可能还记得在基础光照一小节中介绍了将法线变换到世界空间下不能简单地直接乘以模型矩阵原因在这篇文章中进行了详细介绍译文。这里的原因很相似问题在于投影矩阵对坐标进行了非正交变换导致改变了顶点之间的相对距离与方向使得最后计算出来的法线与物体表面不再垂直。正确的做法是在几何着色器中使用原始的顶点信息来进行法线计算。 在顶点着色器中输出原始顶点位置信息 #version 330 core layout (location 0) in vec3 aPos; layout (location 2) in vec2 aTexCoords;out VS_OUT {vec2 texCoords;vec3 position; } vs_out;void main() {vs_out.texCoords aTexCoords;vs_out.position aPos;gl_Position vec4(aPos, 1.0); }在几何着色器中使用原始顶点位置计算法线直接偏移原始顶点位置然后对偏移后的原始顶点位置进行 MVP 变换 #version 330 core layout (triangles) in; layout (triangle_strip, max_vertices 3) out;in VS_OUT {vec2 texCoords;vec3 position; } gs_in[];out vec2 TexCoords;uniform float time; uniform mat4 projection; uniform mat4 view; uniform mat4 model;vec3 GetNormal() {vec3 a gs_in[0].position - gs_in[1].position;vec3 b gs_in[2].position - gs_in[1].position;return normalize(cross(a, b)); }vec4 explode(vec3 position, vec3 normal) {float magnitude 2.0;vec3 direction normal * ((sin(time) 1.0) / 2.0) * magnitude; return vec4(position direction, 1.f); }void main() { vec3 normal GetNormal();mat4 mvp projection * view * model;gl_Position mvp * explode(gs_in[0].position, normal);TexCoords gs_in[0].texCoords;EmitVertex();gl_Position mvp * explode(gs_in[1].position, normal);TexCoords gs_in[1].texCoords;EmitVertex();gl_Position mvp * explode(gs_in[2].position, normal);TexCoords gs_in[2].texCoords;EmitVertex();EndPrimitive(); }项目源码爆炸 - GitCode 法向量可视化 在这一部分中我们将使用几何着色器来实现一个实用的例子显示任意物体的法向量。在编写光照着色器时你可能会遇到一些奇怪的视觉输出但又很难确定问题的原因。光照错误通常是由于错误的法向量引起的这可能是因为不正确地加载顶点数据、错误地将其定义为顶点属性或在着色器中不正确地管理所致。我们需要某种方式来检测提供的法向量是否正确。可视化法向量是检查其正确性的好方法而几何着色器正是实现此目的的有用工具。 我们的思路是首先我们不使用几何着色器正常绘制场景。然后再次绘制场景但这次只显示通过几何着色器生成的法向量。几何着色器接收一个三角形图元并沿着法向量生成三条线——每个顶点一条法向量。伪代码如下所示 shader.use(); DrawScene();normalDisplayShader.use(); DrawScene();这次在几何着色器中我们将使用模型提供的顶点法线而不是自己生成。为了适应观察和模型矩阵的缩放和旋转我们在将法线变换到观察空间坐标之前先使用法线矩阵变换一次几何着色器接收的位置向量是观察空间坐标因此我们应该将法向量变换到相同的空间中。这可以在顶点着色器中完成 #version 330 core layout (location 0) in vec3 aPos; layout (location 1) in vec3 aNormal;out VS_OUT {vec3 normal; } vs_out;uniform mat4 view; uniform mat4 model;void main() {gl_Position view * model * vec4(aPos, 1.0); mat3 normalMatrix mat3(transpose(inverse(view * model)));vs_out.normal normalize(vec3(vec4(normalMatrix * aNormal, 0.0))); }变换后的观察空间法向量会以接口块的形式传递到下一个着色器阶段。接下来几何着色器会接收每个顶点包括一个位置向量和一个法向量并在每个位置向量处绘制一个法线向量 #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;uniform mat4 projection;void GenerateLine(int index) {gl_Position projection * gl_in[index].gl_Position;EmitVertex();gl_Position projection * (gl_in[index].gl_Position vec4(gs_in[index].normal, 0.0) * MAGNITUDE);EmitVertex();EndPrimitive(); }void main() { GenerateLine(0); // 第一个顶点法线GenerateLine(1); // 第二个顶点法线GenerateLine(2); // 第三个顶点法线 }像这样的几何着色器应该很容易理解。注意我们将法向量乘以了一个 MAGNITUDE 向量来限制显示的法向量大小否则它们会有点大 由于法线的可视化通常用于调试目的我们可以使用片段着色器将其显示为单色线如果你愿意也可以是非常漂亮的线 #version 330 core out vec4 FragColor;void main() {FragColor vec4(1.0, 1.0, 0.0, 1.0); }现在首先使用普通着色器渲染模型然后再使用特殊的法线可视化着色器渲染你将看到这样的效果 除了让我们的背包变得毛茸茸之外它还能让我们很好地判断模型的法向量是否准确。你可以想象到这样的几何着色器也经常用于给物体添加毛发(Fur)。 你可以在这里找到源码。 项目源码法线可视化 - GitCode 实例化 原文链接实例化 - LearnOpenGL CN 基础 假设你有一个绘制了许多模型的场景其中大部分模型包含同一组顶点数据但应用了不同的世界空间变换。想象一个充满草的场景每根草都是一个包含几个三角形的小模型。你可能需要绘制大量的草最终可能需要在每帧中渲染数千甚至数万根草。由于每根草仅由几个三角形构成因此渲染几乎是瞬间完成的但数千个渲染函数调用会极大地影响性能。 渲染大量物体时的性能瓶颈 如果我们渲染大量物体代码可能如下所示 for (unsigned int i 0; i amount_of_models_to_draw; i) {DoSomePreparations(); // 绑定 VAO、绑定纹理、设置 uniform 等glDrawArrays(GL_TRIANGLES, 0, amount_of_vertices); }像这样绘制模型的大量实例很快就会因为绘制调用过多而达到性能瓶颈。与绘制顶点本身相比使用 glDrawArrays 或 glDrawElements 函数告诉 GPU 绘制顶点数据会消耗更多性能因为 OpenGL 在绘制顶点数据之前需要做很多准备工作例如告诉 GPU 从哪个缓冲区读取数据从哪里寻找顶点属性而这些都是在相对缓慢的 CPU 到 GPU 总线(CPU to GPU Bus)上传输的。因此即使渲染顶点非常快命令 GPU 渲染也未必如此。 实例化Instancing技术 如果我们能够将数据一次性发送给 GPU然后使用一个绘制函数让 OpenGL 利用这些数据绘制多个物体那就更方便了。这就是实例化Instancing。 实例化技术允许我们使用一个渲染调用来绘制多个物体从而节省每次绘制物体时 CPU - GPU 的通信开销它只需要一次即可。要使用实例化渲染我们只需要将 glDrawArrays 和 glDrawElements 的渲染调用分别改为 glDrawArraysInstanced 和 glDrawElementsInstanced 即可。这些渲染函数的实例化版本需要一个额外的参数称为实例数量Instance Count它用于设置我们需要渲染的实例个数。这样我们只需要将必要的数据发送到 GPU 一次然后使用一次函数调用告诉 GPU 它应该如何绘制这些实例。GPU 将直接渲染这些实例而无需不断与 CPU 进行通信。 gl_InstanceID 内建变量 实例化渲染函数本身并没有什么特殊作用。渲染同一个物体一千次对我们并没有什么用处因为每个物体都是完全相同的而且还在同一个位置。我们只能看到一个物体出于这个原因GLSL 在顶点着色器中嵌入了另一个内建变量gl_InstanceID。 在使用实例化渲染调用时gl_InstanceID 会从 0 开始在每个实例被渲染时递增 1。例如如果我们正在渲染第 43 个实例那么顶点着色器中它的 gl_InstanceID 将是 42。由于每个实例都有唯一的 ID我们可以创建一个数组将 ID 与位置值对应起来将每个实例放置在世界的不同位置。 实例化绘制示例 为了体验实例化绘制我们将在标准化设备坐标系中使用一个渲染调用绘制 100 个 2D 四边形。我们会索引一个包含 100 个偏移向量的 uniform 数组将偏移值添加到每个实例化的四边形上。最终的结果是一个排列整齐的四边形网格 每个四边形由 2 个三角形组成共有 6 个顶点。每个顶点包含一个 2D 的标准化设备坐标位置向量和一个颜色向量。以下是本例使用的顶点数据为了填充整个屏幕每个三角形都很小 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 }; 片段着色器将从顶点着色器接收颜色向量并将其设置为颜色输出以实现四边形的着色 #version 330 core out vec4 FragColor;in vec3 fColor;void main() {FragColor vec4(fColor, 1.0); }到目前为止还没有什么新内容但从顶点着色器开始情况就变得有趣起来 #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; }这里我们定义了一个名为 offsets 的数组它包含 100 个偏移向量。在顶点着色器中我们将使用 gl_InstanceID 来索引 offsets 数组获取每个实例的偏移向量。如果我们要实例化绘制 100 个四边形仅使用此顶点着色器我们就可以得到 100 个位于不同位置的四边形。 目前我们仍然需要设置这些偏移位置。我们将在进入渲染循环之前使用一个嵌套的 for 循环来计算 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;} }在这里我们创建了 100 个位移向量表示 10x10 网格上的所有位置。除了生成 translations 数组之外我们还需要将数据传输到顶点着色器的 uniform 数组中 shader.use();for (unsigned int i 0; i 100; i) {shader.setVec2((offsets[ std::to_string(i) ]), translations[i]); }在这段代码中我们将 for 循环的计数器 i 转换为一个字符串该字符串可用于动态创建位置值的字符串以索引 uniform 位置值。接下来我们将为 offsets uniform 数组中的每一项设置相应的位移向量。 现在所有准备工作都已完成我们可以开始渲染四边形了。对于实例化渲染我们使用 glDrawArraysInstanced 或 glDrawElementsInstanced。由于我们没有使用索引缓冲区因此我们将调用 glDrawArrays 版本的函数 glBindVertexArray(quadVAO); glDrawArraysInstanced(GL_TRIANGLES, 0, 6, 100);lDrawArraysInstanced 的参数与 glDrawArrays 完全相同除了最后一个参数用于设置需要绘制的实例数量。因为我们想在 10x10 网格中显示 100 个四边形所以我们将其设置为 100。运行代码后您应该会看到熟悉的 100 个彩色四边形。 实例化数组 虽然之前的实现在目前情况下能够正常工作但是如果我们要渲染远超 100 个实例时这其实非常普遍我们最终会超过最大能够发送至着色器的 uniform 数据大小上限。一个替代方案是实例化数组Instanced Array它被定义为一个顶点属性可以让我们存储更多的数据仅在顶点着色器渲染一个新实例时才会更新。 使用顶点属性时顶点着色器的每次运行都会让 GLSL 获取一组适用于当前顶点的新属性。而当我们将顶点属性定义为一个实例化数组时顶点着色器只需要对每个实例而不是每个顶点更新顶点属性的内容。这允许我们对逐顶点的数据使用普通的顶点属性而对逐实例的数据使用实例化数组。 为了给你一个实例化数组的例子我们将使用之前的例子并将偏移量 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; }我们不再使用 gl_InstanceID现在不需要索引一个 uniform 数组就能够直接使用 offset 属性了。 由于实例化数组和 position 与 color 变量一样都是顶点属性我们还需要将其内容存储在顶点缓冲对象中并配置其属性指针。我们首先将上一部分的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);之后我们还需要设置它的顶点属性指针并启用顶点属性 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);这段代码有趣的地方在于最后一行我们调用了 glVertexAttribDivisor。这个函数告诉 OpenGL 何时更新顶点属性的内容到新的一组数据。 它的第一个参数是所需的顶点属性第二个参数是属性除数Attribute Divisor。 默认情况下属性除数是 0告诉 OpenGL 我们需要在顶点着色器的每次迭代时更新顶点属性。将其设置为 1 时我们告诉 OpenGL 我们希望在渲染一个新实例的时候更新顶点属性。而设置为 2 时我们希望每 2 个实例更新一次属性以此类推。 我们将属性除数设置为 1是在告诉 OpenGL位于位置 2 的顶点属性是一个实例化数组。如果我们现在使用 glDrawArraysInstanced 再次渲染四边形会得到以下输出 这和之前的例子完全相同但这次是使用实例化数组实现的。这使我们能够将更多数据只要内存允许传递到顶点着色器以用于实例化绘制。 为了更有趣一点我们还可以使用 gl_InstanceID从右上角到左下角逐渐缩小四边形 void main() {vec2 pos aPos * (gl_InstanceID / 100.0);gl_Position vec4(pos aOffset, 0.0, 1.0);fColor aColor; }结果是第一个四边形实例会非常小随着绘制实例的增加gl_InstanceID 会越来越接近 100四边形也越来越接近原始大小。像这样将实例化数组与 gl_InstanceID 结合使用是完全可行的。 如果你还是不确定实例化渲染是如何工作的或者想看看所有代码是如何组合起来的你可以在这里找到程序的源代码。 虽然很有趣但是这些例子并不是实例化的好例子。是的它们的确让你知道实例化是怎么工作的但是我们还没接触到它最有用的一点绘制巨大数量的相似物体。出于这个原因我们将会在下一部分进入太空探险见识实例化渲染真正的威力。 小行星带 普通渲染 想象这样一个场景在宇宙中有一个大的行星它位于小行星带的中央。这样的小行星带可能包含成千上万的岩块在很不错的显卡上也很难完成这样的渲染。实例化渲染正是适用于这样的场景因为所有的小行星都可以使用一个模型来表示。每个小行星可以再使用不同的变换矩阵来进行少许的变化。 为了展示实例化渲染的作用我们首先会不使用实例化渲染来渲染小行星绕着行星飞行的场景。这个场景将会包含一个大的行星模型它可以在这里下载以及很多环绕着行星的小行星。小行星的岩石模型可以在这里下载。 在代码例子中我们将使用在模型加载小节中定义的模型加载器来加载模型。 为了得到想要的效果我们将为每个小行星生成一个变换矩阵作为其模型矩阵。变换矩阵首先将小行星位移到小行星带中的某个位置我们还会添加一个小的随机偏移值到该位移上使圆环看起来更自然。然后我们应用一个随机缩放并以一个随机旋转向量为轴进行随机旋转。最终的变换矩阵不仅能将小行星变换到行星周围还能使其看起来更自然与其他小行星区分开来。最终结果是一个布满小行星的圆环每个小行星都独一无二。 unsigned int amount 1000; glm::mat4 *modelMatrices; modelMatrices new glm::mat4[amount]; srand(glfwGetTime()); // 初始化随机种子 float radius 50.0f; float offset 2.5f;for (unsigned int i 0; i amount; i) {glm::mat4 model(1.f);// 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.05f;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; }这段代码可能看起来有点复杂但我们只是将小行星的 x 和 z 位置变换到一个半径为 radius 的圆形上并在半径的基础上偏移了 -offset 到 offset。我们让 y 偏移的影响更小一点使小行星带更扁平。然后我们应用了缩放和旋转变换并将最终的变换矩阵存储在 modelMatrices 中该数组的大小为 amount。这里我们总共生成 1000 个模型矩阵每个小行星一个。 加载完行星和岩石模型并编译完着色器后渲染代码如下所示 // 绘制行星 shader.use(); glm::mat4 model(1.f); model glm::translate(model, glm::vec3(0.0f, -3.0f, 0.0f)); model glm::scale(model, glm::vec3(4.0f, 4.0f, 4.0f)); shader.setMat4(model, model); planet.Draw(shader);// 绘制小行星 for (unsigned int i 0; i amount; i) {shader.setMat4(model, modelMatrices[i]);rock.Draw(shader); }我们首先绘制了行星模型并对其进行位移和缩放以适应场景。然后我们绘制了 amount 个岩石模型。在绘制每个岩石之前我们需要在着色器内设置相应的模型变换矩阵。 最终结果是一个看起来像太空的场景环绕行星的是一个看起来很自然的小行星带 这个场景每帧包含1001次渲染调用其中1000个是岩石模型。你可以在这里找到源代码。 当我们开始增加这个数字的时候你很快就会发现场景不再能够流畅运行了帧数也下降很厉害。当我们将amount设置为2000的时候场景就已经慢到移动都很困难的程度了。 我们计算帧数显示在窗口标题上首先在渲染循环外定义几个新变量 int frameCount 0; // 帧计数器 double fpsInterval 0.5; // 计算FPS的时间间隔单位为秒 double timeElapsed 0.0; // 自上次计算FPS以来经过的时间然后在渲染循环内计算 fps 并显示在标题上 float currentFrame glfwGetTime(); deltaTime currentFrame - lastFrame; lastFrame currentFrame;timeElapsed deltaTime; frameCount; if (timeElapsed fpsInterval) {// 计算FPSdouble fps frameCount / timeElapsed;glfwSetWindowTitle(window, (LearnOpenGL FPS: std::to_string(frameCount)).c_str());// 重置计数器frameCount 0;timeElapsed - fpsInterval; }整体场景较大调整了 camera 的移动速度为 15.f并设定初始位置为 glm::vec3(0.0f, 10.f, 80.0f)。 当 rock 调整到 10000fps 就只有 13 左右了 项目源码正常渲染 - GitCode 实例化渲染 现在我们尝试使用实例化渲染来渲染相同的场景。我们首先对顶点着色器进行一点修改 #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; }我们不再使用模型 uniform 变量而是改用一个 mat4 类型的顶点属性这样我们就可以存储一个实例化数组的变换矩阵。但是当顶点属性的类型大于 vec4 时需要多进行一步处理。顶点属性允许的最大数据大小等于一个 vec4。由于一个 mat4 本质上是 4 个 vec4我们需要为这个矩阵预留 4 个顶点属性。因为我们将其位置值设置为 3所以矩阵每一列的顶点属性位置值就是 3、4、5 和 6。 接下来我们需要为这 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*)(1 * 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); }注意这里我们将 Mesh 的 VAO 从私有变量更改为公共变量以便我们能够访问其顶点数组对象。这不是最佳解决方案只是为了配合本节的一个简单更改。除此之外代码应该很清楚了。我们告诉 OpenGL 如何解释每个缓冲顶点属性的缓冲区并告诉它这些顶点属性是实例化数组。 接下来我们再次使用网格的 VAO这一次使用 glDrawElementsInstanced 进行绘制 // 绘制小行星 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); }这里我们绘制与之前相同数量 (amount) 的小行星但使用的是实例渲染。结果应该非常相似但如果你开始增加 amount 变量你就能看到实例化渲染的效果了。没有实例化渲染时我们只能流畅渲染 1000 到 1500 个小行星。而使用实例化渲染后我们可以将这个值设置为 100000每个岩石模型有 576 个顶点每帧加起来大概要绘制 5700 万个顶点但性能却没有受到任何影响 上面这幅图渲染了10万个小行星在某些机器上10 万个小行星可能会太多了所以尝试修改这个值直到达到一个你能接受的帧率半径为 150.0f偏移量等于 25.0f。你可以在这里找到实例化渲染的代码。 可以看到在合适的环境下实例化渲染能够大大增加显卡的渲染能力。正是出于这个原因实例化渲染通常会用于渲染草、植被、粒子以及上面这样的场景基本上只要场景中有很多重复的形状都能够使用实例化渲染来提高性能。 项目源码实例化渲染 - GitCode 抗锯齿 原文链接抗锯齿 - LearnOpenGL CN 在学习渲染的旅途中你可能会时不时遇到模型边缘有锯齿的情况。这些锯齿边缘(Jagged Edges)的产生和光栅器将顶点数据转化为片段的方式有关。在下面的例子中你可以看到我们只是绘制了一个简单的立方体你就能注意到它存在锯齿边缘了 可能不是非常明显但如果你离近仔细观察立方体的边缘你就应该能够看到锯齿状的图案。如果放大的话你会看到下面的图案 显然这不是我们在最终程序中想要实现的效果。你可以清楚地看到边缘形成的像素。这种现象称为走样Aliasing。有许多种抗锯齿Anti-aliasing也称为反走样技术可以帮助我们缓解这种现象从而产生更平滑的边缘。 最初我们有一种称为超采样抗锯齿Super Sample Anti-aliasingSSAA的技术它会使用比正常分辨率更高的分辨率即超采样来渲染场景当图像输出在帧缓冲区中更新时分辨率会被下采样Downsample至正常分辨率。这些额外的分辨率被用来防止锯齿边缘的产生。虽然它确实能够解决走样问题但是由于这样比平时要绘制更多的片段它也会带来很大的性能开销。所以这项技术只拥有了短暂的辉煌。 然而在这项技术的基础上也诞生了更为现代的技术称为多重采样抗锯齿Multisample Anti-aliasingMSAA。它借鉴了 SSAA 背后的理念但却以更高效的方式实现了抗锯齿。我们将在本节中深入讨论 OpenGL 中内置的 MSAA 技术。 多重采样 要理解多重采样Multisampling及其解决锯齿问题的原理我们需要深入了解 OpenGL 光栅化器的工作方式。光栅化器是位于顶点处理阶段之后、片段着色器之前的整个算法和过程的总和。它以图元的所有顶点作为输入并将其转换为一系列片段。顶点坐标理论上可以取任意值但片段坐标受限于窗口分辨率因此只能取离散值。由于顶点坐标与片段坐标之间几乎不存在一一映射关系因此光栅化器必须决定每个顶点最终对应的片段/屏幕坐标。 这里我们可以看到一个屏幕像素的网格每个像素的中心包含一个采样点Sample Point它被用来决定三角形是否覆盖了某个像素。图中红色的采样点被三角形覆盖在每个被覆盖的像素处都会生成一个片段。虽然三角形边缘的一些部分也覆盖了某些屏幕像素但是这些像素的采样点并没有被三角形内部覆盖所以它们不会受到片段着色器的影响。 现在你可能已经清楚走样Aliasing的原因了。完整渲染后的三角形在屏幕上会是这样的 由于屏幕像素总量的限制一些边缘像素可以被渲染出来而另一些则不会。结果是我们使用不光滑的边缘来渲染图元导致之前讨论的锯齿边缘。 多重采样Multisampling所做的正是将单一采样点变为多个采样点这也是它名称的由来。我们不再使用像素中心的单一采样点而是使用以特定图案排列的 4 个子采样点Subsample取而代之。我们将用这些子采样点来决定像素的覆盖度。 上图左侧展示了正常情况下判定三角形是否覆盖像素的方式。在示例中该像素不会运行片段着色器因此它将保持空白因为它的采样点未被三角形覆盖。上图右侧展示了实施多重采样后的版本每个像素包含 4 个采样点。这里只有两个采样点覆盖了三角形。 采样点的数量可以是任意的更多的采样点能带来更精确的覆盖率。 从这里开始多重采样就变得有趣起来了。我们知道三角形只覆盖了 2 个子采样点所以下一步是决定这个像素的颜色。你可能会猜测我们对每个被覆盖的子采样点运行一次片段着色器最后将每个像素所有子采样点的颜色平均一下。在这个例子中我们需要在两个子采样点上对被插值的顶点数据运行两次片段着色器并将结果颜色存储在这些采样点中。幸运的是这并不是它的工作方式因为这本质上说还是需要运行更多次的片段着色器会显著降低性能。 MSAA 真正的工作方式是无论三角形覆盖了多少个子采样点每个图元中每个像素只运行一次片段着色器。片段着色器所使用的顶点数据来自像素中心的插值然后MSAA 使用更大的深度/模板缓冲区来确定子采样点的覆盖率。被覆盖的子采样点数量将决定像素颜色对帧缓冲区的影响程度。因为上图的 4 个采样点中只有 2 个被覆盖所以三角形的颜色会有一半与帧缓冲区的颜色这里是无色进行混合最终形成一种淡蓝色。 这样做之后颜色缓冲区中所有图元边缘将产生更平滑的图形。让我们来看看前面三角形的多重采样会是什么样子 这里每个像素包含 4 个子采样点未标注不相关的采样点蓝色采样点被三角形覆盖而灰色采样点则未被覆盖。对于三角形内部的像素片段着色器只会运行一次颜色输出将存储到所有 4 个子采样点中。而在三角形边缘并非所有子采样点都被覆盖因此片段着色器的结果将仅存储到部分子采样点中。根据被覆盖的子采样点数量最终像素颜色将由三角形颜色与其他子采样点中存储的颜色决定。 简单来说一个像素中被三角形覆盖的采样点越多该像素的颜色就越接近三角形的颜色。如果我们给上面的三角形填充颜色就能得到以下效果 三角形不平滑的边缘被稍浅的颜色包围后从远处观察会显得更平滑。 深度值和模板值会按各个子采样点存储。当多个三角形重叠在单个像素上时即使我们只运行一次片段着色器颜色值也依然会按子采样点存储。对于深度测试在运行深度测试之前每个顶点的深度值会被插值到各个子采样点中。对于模板测试我们会为每个子采样点存储模板值这意味着缓冲区的大小会根据每个像素的子采样点数量而增加。 我们目前讨论的都是多重采样抗锯齿背后的原理光栅化器背后的实际逻辑比目前讨论的要复杂。但你现在应该可以理解多重采样抗锯齿的大体概念和逻辑了。 (译者注 如果看到这里还是对原理似懂非懂可以简单看看知乎上 文刀秋二对抗锯齿技术的精彩介绍) OpenGL中的MSAA 如果我们想在 OpenGL 中使用 MSAA就必须使用一个能在每个像素中存储多个颜色值的颜色缓冲区因为多重采样需要我们为每个采样点都存储一个颜色。因此我们需要一种新的缓冲区类型来存储特定数量的多重采样样本它被称为多重采样缓冲区Multisample Buffer。 大多数窗口系统都提供了一个多重采样缓冲区用于代替默认的颜色缓冲区。GLFW 同样提供了此功能我们所要做的只是提示 GLFW我们希望使用一个包含 N 个样本的多重采样缓冲区。这可以在创建窗口之前调用 glfwWindowHint 来完成。 glfwWindowHint(GLFW_SAMPLES, 4);现在再调用 glfwCreateWindow 创建渲染窗口时每个屏幕坐标就会使用一个包含 4 个子采样点的颜色缓冲区了。GLFW 会自动创建一个每像素 4 个子采样点的深度和模板缓冲区。这也意味着所有缓冲区的大小都增长了 4 倍。 现在我们已经向 GLFW 请求了多重采样缓冲区还需要调用 glEnable 并启用 GL_MULTISAMPLE来启用多重采样。在大多数 OpenGL 驱动上多重采样都是默认启用的所以这个调用可能会有点多余但显式地调用一下会更保险一点。这样子不论是什么 OpenGL 的实现都能够正常启用多重采样了。 glEnable(GL_MULTISAMPLE);由于多重采样的算法都在 OpenGL 驱动的光栅化器中实现我们不需要再多做什么。如果现在再来渲染本节一开始的那个绿色立方体我们应该能看到更平滑的边缘 这个箱子看起来的确要平滑多了如果在场景中有其它的物体它们也会看起来平滑很多。你可以在这里找到这个简单例子的源代码。 离屏MSAA 由于 GLFW 负责创建多重采样缓冲区启用 MSAA 非常简单。然而如果我们想使用自己的帧缓冲区进行离屏渲染就必须自己生成多重采样缓冲区。现在我们确实需要自己创建多重采样缓冲区。 有两种方式可以创建多重采样缓冲区并将其作为帧缓冲区的附件纹理附件和渲染缓冲区附件这与在帧缓冲教程中讨论的普通附件非常相似。 多重采样纹理附件 为了创建一个支持存储多个采样点的纹理我们使用 glTexImage2DMultisample 来替代 glTexImage2D其纹理目标为 GL_TEXTURE_2D_MULTISAMPLE。 glTexImage2DMultisample(GLenum target, GLsizei samples, GLenum internalformat, GLsizei width, GLsizei height, GLboolean fixedsamplelocations);glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, tex); glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, samples, GL_RGB, width, height, GL_TRUE); glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, 0);其第二个参数设置的是纹理所拥有的样本个数。如果最后一个参数为 GL_TRUE图像将为每个纹素使用相同的样本位置以及相同数量的子采样点。 我们同样使用 glFramebufferTexture2D 将多重采样纹理附加到帧缓冲区上但这里纹理类型使用的是 GL_TEXTURE_2D_MULTISAMPLE。 glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D_MULTISAMPLE, tex, 0);当前绑定的帧缓冲区现在拥有了一个纹理图像形式的多重采样颜色缓冲区。 多重采样渲染缓冲对象 与纹理类似创建一个多重采样渲染缓冲区对象并不困难。我们所要做的只是在指定当前绑定的渲染缓冲区的内存存储时将 glRenderbufferStorage 的调用替换为 glRenderbufferStorageMultisample 即可。 glRenderbufferStorageMultisample(GL_RENDERBUFFER, 4, GL_DEPTH24_STENCIL8, width, height);在此函数中渲染缓冲区对象后的参数用于设置样本数量在本例中为 4。 渲染到多重采样帧缓冲 渲染到多重采样帧缓冲区对象的过程非常简单。只要我们在帧缓冲区绑定时绘制任何内容光栅化器就会负责所有的多重采样运算。我们最终会得到一个多重采样颜色缓冲区以及/或深度和模板缓冲区。因为多重采样缓冲区有点特别我们不能直接将它们的缓冲区图像用于其他运算比如在着色器中对它们进行采样。 一个多重采样的图像包含比普通图像更多的信息我们所要做的是缩小或还原Resolve图像。多重采样帧缓冲区的还原通常通过 glBlitFramebuffer 来完成它可以将一个帧缓冲区中的某个区域复制到另一个帧缓冲区中并且将多重采样缓冲区还原。 glBlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter)srcX0, srcY0: 指定源帧缓冲区矩形区域的一个角的x和y坐标。srcX1, srcY1: 指定源帧缓冲区矩形区域对角位置的x和y坐标。这两个坐标与前两个一起定义了从哪个区域读取数据。dstX0, dstY0: 指定目标帧缓冲区矩形区域的一个角的x和y坐标。dstX1, dstY1: 指定目标帧缓冲区矩形区域对角位置的x和y坐标。这两个坐标与前两个一起定义了数据将被写入哪个区域。mask: 一个位字段指定要复制哪些缓冲区。它可以是 GL_COLOR_BUFFER_BIT、GL_DEPTH_BUFFER_BIT 和 GL_STENCIL_BUFFER_BIT 的任意组合分别表示颜色缓冲区、深度缓冲区和模板缓冲区。filter: 指定使用的插值过滤器。可以是 GL_NEAREST 或 GL_LINEAR。GL_NEAREST 表示使用最邻近插值而 GL_LINEAR 则表示使用线性插值。 glBlitFramebuffer 会将一个用 4 个屏幕空间坐标定义的源区域复制到一个同样用 4 个屏幕空间坐标定义的目标区域中。你可能记得在帧缓冲区教程中当我们绑定到 GL_FRAMEBUFFER 时我们是同时绑定了读取和绘制的帧缓冲区目标。我们也可以将帧缓冲区分开绑定至 GL_READ_FRAMEBUFFER 与 GL_DRAW_FRAMEBUFFER。glBlitFramebuffer 函数会根据这两个目标决定哪个是源帧缓冲区哪个是目标帧缓冲区。 接下来我们可以将图像位块传送Blit到默认的帧缓冲区中将多重采样的帧缓冲区传送到屏幕上。 glBindFramebuffer(GL_READ_FRAMEBUFFER, multisampledFBO); glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST);如果现在再来渲染这个程序我们会得到与之前完全一样的结果一个使用 MSAA 显示出来的橄榄绿色立方体且锯齿边缘明显减少。 项目源码离屏MSAA - GitCode 但如果我们想使用多重采样帧缓冲区的纹理输出来进行后期处理等操作呢我们不能直接在片段着色器中使用多重采样纹理。但我们可以将多重采样缓冲区位块传输到一个未使用多重采样纹理附件的 FBO 中。然后使用这个普通的颜色附件进行后期处理从而达到我们的目的。然而这也意味着我们需要生成一个新的 FBO作为中介帧缓冲区对象将多重采样缓冲区还原为一个可以在着色器中使用的普通 2D 纹理。这个过程的伪代码如下 unsigned int msFBO CreateFBOWithMultiSampledAttachments(); // 使用普通的纹理颜色附件创建一个新的FBO ... glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, screenTexture, 0); ... while(!glfwWindowShouldClose(window)) {...glBindFramebuffer(msFBO);ClearFrameBuffer();DrawScene();// 将多重采样缓冲还原到中介FBO上glBindFramebuffer(GL_READ_FRAMEBUFFER, msFBO);glBindFramebuffer(GL_DRAW_FRAMEBUFFER, intermediateFBO);glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST);// 现在场景是一个2D纹理缓冲可以将这个图像用来后期处理glBindFramebuffer(GL_FRAMEBUFFER, 0);ClearFramebuffer();glBindTexture(GL_TEXTURE_2D, screenTexture);DrawPostProcessingQuad(); ... }如果现在再实现帧缓冲区教程中的后期处理效果我们就能够在几乎没有锯齿的场景纹理上进行后期处理了。如果将图像灰度化效果如下 由于屏幕纹理又变回了一个只有单一采样点的普通纹理像边缘检测这样的后期处理滤镜会重新导致锯齿。为了补偿这个问题你可以之后对纹理进行模糊处理或者设计你自己的抗锯齿算法。 你可以看到如果将多重采样与离屏渲染结合起来我们需要自己负责一些额外的细节。但所有这些细节都值得付出额外的努力因为多重采样能够显著提升场景的视觉质量。当然要注意如果使用的采样点非常多启用多重采样会显著降低程序的性能。在本节写作时通常采用 4 采样点的 MSAA。 你可以在这里找到源代码。 当通过glBlitFramebuffer将多重采样源FBO复制到非多重采样目标FBO时OpenGL会自动执行解析操作 合并方式将每个像素的多个样本如4个按规则如求平均合并为单个颜色值。结果生成一个平滑的抗锯齿图像存储在普通2D纹理中。 // 示例将4x MSAA的FBO解析到普通FBO glBindFramebuffer(GL_READ_FRAMEBUFFER, msaa_fbo); glBindFramebuffer(GL_DRAW_FRAMEBUFFER, resolve_fbo); glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST);多重采样并进行后期处理的流程 原始渲染4x MSAA→ [多重采样FBO每个像素4个样本]→ glBlitFramebuffer解析→ [普通FBO每个像素1个合并值]→ 后期处理片段着色器直接采样项目源码后期处理 - GitCode 同时需要注意一个限制条件见官方文档当源帧缓冲区GL_READ_FRAMEBUFFER或目标帧缓冲区GL_DRAW_FRAMEBUFFER是多重采样缓冲区即它们的 GL_SAMPLE_BUFFERS 属性为1时 - 必须满足源区域和目标区域的宽度和高度完全相同。 - 否则会触发 GL_INVALID_OPERATION 错误操作失败。 多重采样缓冲区的本质是每个像素包含多个样本如4x MSAA时每个像素有4个样本。当使用glBlitFramebuffer进行复制时 如果源或目标是多重采样的OpenGL需要确保样本数据能正确解析Resolve或映射到目标缓冲区。缩放或拉伸如将1024x768的源复制到512x384的目标会涉及像素插值或重采样但多重采样的样本分布与几何覆盖密切相关直接缩放会导致样本解析逻辑混乱无法保证抗锯齿效果的正确性。因此OpenGL强制要求当涉及多重采样缓冲区时复制区域的尺寸必须严格一致避免因缩放引入未定义行为。 场景1解析多重采样缓冲区常见操作 源4x MSAA的多重采样FBO尺寸1920x1080目标普通2D纹理FBO尺寸1920x1080操作合法通过glBlitFramebuffer将多重采样的源解析到非多重采样目标尺寸一致。 场景2尝试缩放多重采样缓冲区 源4x MSAA的多重采样FBO尺寸1920x1080目标普通2D纹理FBO尺寸960x540操作非法触发GL_INVALID_OPERATION因为目标尺寸与源不同。 如果需要将多重采样缓冲区的输出缩放到不同尺寸需分两步操作 解析到相同尺寸的非多重采样缓冲区// 步骤1解析多重采样到普通纹理尺寸一致 glBlitFramebuffer(0, 0, 1920, 1080, 0, 0, 1920, 1080, GL_COLOR_BUFFER_BIT, GL_LINEAR);对普通纹理进行缩放// 步骤2将普通纹理复制到更小的目标此时允许缩放 glBlitFramebuffer(0, 0, 1920, 1080, 0, 0, 960, 540, GL_COLOR_BUFFER_BIT, GL_LINEAR);自定义抗锯齿算法 将多重采样的纹理图像直接传递给着色器而不进行还原操作也是可行的。GLSL 提供了这样的选项允许我们对纹理图像的每个子采样点进行采样因此我们可以创建自己的抗锯齿算法。在大型图形应用程序中通常会采用这种方法。 要获取每个子采样点的颜色值你需要将纹理 uniform 采样器设置为 sampler2DMS而不是常用的 sampler2D uniform sampler2DMS screenTextureMS;使用 texelFetch 函数可以获取每个子采样点的颜色值 vec4 colorSample texelFetch(screenTextureMS, TexCoords, 3); // 第 4 个子采样点我们不会深入探讨自定义抗锯齿技术的细节这里仅提供一些启发。
http://www.tj-hxxt.cn/news/234093.html

相关文章:

  • 网站 上一篇 下一篇wordpress小工具视频
  • 北京哪里做网站好网页升级紧急通知写作
  • 网站建设平ppt程序员培训机构出来找工作好找吗
  • 黔南州建设局门户网站手机端网站建设备案
  • 广州专业的网站制作物业管理系统app
  • 做网站建设要学多久微信公众号登录入口官方
  • 昌平网站开发公司建网站需要多少钱
  • 网站开发专业有什么工作购物网站的搜索框用代码怎么做
  • 可以发布广告的网站编辑网站
  • 女网友叫我一起做优惠券网站wordpress安卓显示
  • wordpress主题官方网站深圳市建设工程
  • 哈尔滨模版网站建设wordpress 浏览量 点击
  • 做hmtl的基本网站网络营销分类
  • 网站这么推广怎样做网站二维码
  • 网站开发一个人可以完成吗营销数据网站
  • 先做产品网站还是app未成年做网站
  • 网站建设对比分析龙华新区城市建设局网站
  • 手机视频网站开发教程网站建设需要哪些专业技术
  • 兰州做网站的公司有哪些自由室内设计师接单网站
  • 兰州优化网站公司网站营销推广
  • 网站维护成本怎样才能访问没有备案的网站
  • ftp可以发布网站吗我做的网站怎么打开很慢
  • 北京网站seo报价中国互联网设计公司
  • 怎样做自己的小说网站google在线代理
  • 潍坊公司做网站网址软件下载
  • 网站开发企业培训大宗交易网登录
  • 做的比较好的设计公司网站哪里有做网站app的
  • 加盟济南优化网站的哪家好
  • 国网商旅云网站地址一级域名网站里有二级域名
  • 网站建设开发心得wordpress xiu主题5.4