diff --git a/04 Advanced OpenGL/06 Cubemaps.md b/04 Advanced OpenGL/06 Cubemaps.md new file mode 100644 index 0000000..60de1c9 --- /dev/null +++ b/04 Advanced OpenGL/06 Cubemaps.md @@ -0,0 +1,394 @@ +## Cubemaps + +本文作者JoeyDeVries,由Django翻译自http://learnopengl.com + +我们之前一直使用的是2D纹理,但是还有更多的纹理类型我们没有探索过,本教程中我们讨论的纹理类型是将多个纹理组合起来映射到一个单一纹理,它就是cubemap。 + +基本上说cubemap它包含6个2D纹理,这每个2D纹理是一个立方体(cube)的一个面,也就是说它是一个有贴图的立方体。你可能会奇怪这样的立方体有什么用?为什么费事地把6个独立纹理结合为一个单独的纹理,只使用6个各自独立的不行吗?这是因为cubemap有自己特有的属性,可以使用方向向量对它们索引和采样。想象一下,我们有一个1×1×1的单位立方体,有个以原点为起点的方向向量在它的中心。 + +从cubemap上使用橘黄色向量采样一个纹理值看起来和下图有点像: + +![](http://learnopengl.com/img/advanced/cubemaps_sampling.png) + +!!! Important + + 方向向量的大小无关紧要。一旦提供了方向,OpenGL就会获取方向向量触碰上的相应的纹理像素(texel),这样就返回了正确的纹理采样值。 + + +方向向量触碰上cubemap的一点,意味着我们也可以使用cubemap的位置向量来对cubemap进行采样,只要cubemap位于原点。然后我们就可以获取所有顶点的纹理坐标,就好像cubemap上有顶点位置一样。所获得的结果是一个纹理坐标,这个纹理坐标获取了cubemap上正确的那个面的纹理。 + +创建一个cubemap + +cubemap和其他纹理一样,所以要创建一个cubemap,在进行任何纹理操作之前,需要生成一个纹理,激活相应纹理单元然后绑定到合适的纹理目标上。这次要绑定到GL_TEXTURE_CUBE_MAP: + +```c++ +GLuint textureID; +glGenTextures(1, &textureID); +glBindTexture(GL_TEXTURE_CUBE_MAP, textureID); +``` + +由于cubemap包含6个纹理,每个面一个,我们必须调用glTexImage2D函数6次,函数的参数和前面教程讲的相似。然而这次我们必须把纹理目标(target)参数设置为cubemap特定的面,这是告诉OpenGL我们创建的纹理是对应cubemap哪个面的。如此我们便需要为cubemap的每个面调用一次glTexImage2D了。 + +由于我们有6个面,OpenGL就提供了6个不同的纹理目标,来应对cubemap的面。 + +纹理目标(Texture target) |方位 + ---|--- +GL_TEXTURE_CUBE_MAP_POSITIVE_X | 右 +GL_TEXTURE_CUBE_MAP_NEGATIVE_X | 左 +GL_TEXTURE_CUBE_MAP_POSITIVE_Y | 上 +GL_TEXTURE_CUBE_MAP_NEGATIVE_Y | 下 +GL_TEXTURE_CUBE_MAP_POSITIVE_Z | 后 +GL_TEXTURE_CUBE_MAP_NEGATIVE_Z | 前 + +和很多OpenGL其他枚举一样,对应的int值都是线性增加的,所以我们有一个纹理位置的数组或vector,就可以 GL_TEXTURE_CUBE_MAP_POSITIVE_X为起始来对它们进行遍历,每个迭代枚举值加1,这样循环所有的纹理目标效率较高: + +```c++ + +int width,height; +unsigned char* image; +for(GLuint i = 0; i < textures_faces.size(); i++) +{ + image = SOIL_load_image(textures_faces[i], &width, &height, 0, SOIL_LOAD_RGB); + glTexImage2D( + GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, + 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image + ); +} + +``` + +这儿我们有个vector叫textures_faces,它包含cubemap所需所有纹理位置,并且以上表所列的顺序给出。它为每个当前绑定的cubemp的每个面生成一个纹理。 + +由于cubemap和其他纹理没什么不同,我们也要定义它的放置方式和过滤方式: + +```c++ + +glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR); +glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR); +glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); +glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); +glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); + +``` + +在像素着色器中,我们也必须使用一个不同的采样器——samplerCube,用它来从texture函数中采样,但是这次使用的是一个vec3方向向量,取代vec2。下面是一个像素着色器使用了cubemap的例子: + +```c++ + +in vec3 textureDir; // Direction vector representing a 3D texture coordinate +uniform samplerCube cubemap; // Cubemap texture sampler + +void main() +{ + color = texture(cubemap, textureDir); +} +``` + +看起来不错,但是何必这么做呢?因为恰巧使用cubemap可以简单的实现很多有意思的技术。其中之一便是天空盒(skybox)。 + + + +### 天空盒(Skybox) + +天空盒是一个包裹整个场景的立方体,它由6个图像构成一个环绕的环境,给玩家一种他所在的场景比实际的要大得多的幻觉。比如有些在视频游戏中使用的天空盒的图像是群山、白云或者满天繁星。比如下面的夜空繁星的图像就来自《上古卷轴》3: + +别被GL_TEXTURE_WRAP_R吓到,它只是简单的设置了纹理的R坐标,R坐标对应于纹理的第三个维度(就像位置的z一样)。我们把放置方式设置为GL_CLAMP_TO_EDGE,由于纹理坐标在两个面之间,所以可能并不能触及哪个面(由于硬件限制),因此使用GL_CLAMP_TO_EDGE后OpenGL会返回它们的边界的值,尽管我们可能在两个两个面中间进行的采样。 + +在绘制物体之前,将使用cubemap,而在渲染前我们要激活相应的纹理单元并绑定到cubemap上,这和普通的2D纹理没什么区别。 + +![](http://learnopengl.com/img/advanced/cubemaps_morrowind.jpg) + +你现在可能已经猜到cubemap完全满足天空盒的要求:我们有一个立方体,它有6个面,每个面需要一个贴图。上图中使用了几个夜空的图片给予玩家一种置身广袤宇宙的感觉,可实际上,他还是在一个小盒子之中。 + +网上有很多这样的天空盒的资源。这个网站就提供了很多。这些天空盒图像通常有下面的样式: + +![](http://learnopengl.com/img/advanced/cubemaps_skybox.png) + +如果你把这6个面折叠到一个立方体中,你机会获得模拟了一个巨大的风景的立方体。有些资源所提供的天空盒比如这个例子6个图是连在一起的,你必须手工它们切割出来,不过大多数情况它们都是6个单独的纹理图像。 + +这个细致(高精度)的天空盒就是我们将在场景中使用的那个,你可以在这里下载。 + +#### 加载一个天空盒 + +由于天空盒实际上就是一个cubemap,加载天空盒和之前我们看到的没什么大的不同。为了加载天空盒我们将使用下面的函数,它接收一个包含6个纹理位置的vector: + +```c++ +GLuint loadCubemap(vector faces) +{ + GLuint textureID; + glGenTextures(1, &textureID); + glActiveTexture(GL_TEXTURE0); + + int width,height; + unsigned char* image; + + glBindTexture(GL_TEXTURE_CUBE_MAP, textureID); + for(GLuint i = 0; i < faces.size(); i++) + { + image = SOIL_load_image(faces[i], &width, &height, 0, SOIL_LOAD_RGB); + glTexImage2D( + GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, + GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image + ); + } + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); + glBindTexture(GL_TEXTURE_CUBE_MAP, 0); + + return textureID; +} +``` + +这个函数没什么特别之处。这就是我们前面已经见过的cubemap代码,只不过放进了一个可管理的函数中。 + +然后,在我们调用这个函数之前,我们将把合适的纹理路径加载到一个vector之中,顺序还是按照cubemap枚举的特定顺序: + +```c++ +vector faces; +faces.push_back("right.jpg"); +faces.push_back("left.jpg"); +faces.push_back("top.jpg"); +faces.push_back("bottom.jpg"); +faces.push_back("back.jpg"); +faces.push_back("front.jpg"); +GLuint cubemapTexture = loadCubemap(faces); +``` + +现在我们已经用cubemapTexture作为id把天空盒加载为cubemap。我们现在可以把它绑定到一个立方体来替换不完美的clear color,在前面的所有教程中这个东西做背景已经很久了。 + + + +#### 天空盒的显示 + +因为天空盒绘制在了一个立方体上,我们还需要另一个VAO、VBO以及一组全新的顶点,和任何其他物体一样。你可以从这里获得顶点数据。 + +cubemap用于给3D立方体帖上纹理,可以用立方体的位置作为纹理坐标进行采样。当一个立方体的中心位于原点(0,0,0)的时候,它的每一个位置向量也就是以原点为起点的方向向量。这个方向向量就是我们要得到的立方体某个位置的相应纹理值。出于这个理由,我们只需要提供位置向量,而无需纹理坐标。为了渲染天空盒,我们需要一组新着色器,它们不会太复杂。因为我们只有一个顶点属性,顶点着色器非常简单: + +```c++ +#version 330 core +layout (location = 0) in vec3 position; +out vec3 TexCoords; + +uniform mat4 projection; +uniform mat4 view; + +void main() +{ + gl_Position = projection * view * vec4(position, 1.0); + TexCoords = position; +} +``` + +注意,顶点着色器有意思的地方在于我们把输入的位置向量作为输出给像素着色器的纹理坐标。像素着色器就会把它们作为输入去采样samplerCube: + +```c++ +#version 330 core +in vec3 TexCoords; +out vec4 color; + +uniform samplerCube skybox; + +void main() +{ + color = texture(skybox, TexCoords); +} +``` + +像素着色器比较明了。我们把顶点属性中的位置向量作为纹理的方向向量,使用它们从cubemap采样纹理值。渲染天空盒现在很简单,我们有了一个cubemap纹理,我们简单绑定cubemap纹理,天空盒就自动地用天空盒的cubemap填充了。为了绘制天空盒,我们将把它作为场景中第一个绘制的物体并且关闭深度写入。这样天空盒才能成为所有其他物体的背景来绘制出来。 + +```c++ + +glDepthMask(GL_FALSE); +skyboxShader.Use(); +// ... Set view and projection matrix +glBindVertexArray(skyboxVAO); +glBindTexture(GL_TEXTURE_CUBE_MAP, cubemapTexture); +glDrawArrays(GL_TRIANGLES, 0, 36); +glBindVertexArray(0); +glDepthMask(GL_TRUE); +// ... Draw rest of the scene +``` + +如果你运行程序就会陷入困境,我们希望天空盒以玩家为中心,这样无论玩家移动了多远,天空盒都不会变近,这样就产生一种四周的环境真的非常大的印象。当前的视图矩阵对所有天空盒的位置进行了转转缩放和平移变换,所以玩家移动,cubemap也会跟着移动!我们打算移除视图矩阵的平移部分,这样移动就影响不到天空盒的位置向量了。在基础光照教程里我们提到过我们可以只用4X4矩阵的3×3部分去除平移。我们可以简单地将矩阵转为33矩阵再转回来,就能达到目标 + +```c++ +glm::mat4 view = glm::mat4(glm::mat3(camera.GetViewMatrix())); +``` + +这会移除所有平移,但保留所有旋转,因此用户仍然能够向四面八方看。由于有了天空盒,场景即可变得巨大了。如果你添加些物体然后自由在其中游荡一会儿你会发现场景的真实度有了极大提升。最后的效果看起来像这样: + +![](http://learnopengl.com/img/advanced/cubemaps_skybox_result.png) + +[这里有全部源码](http://learnopengl.com/code_viewer.php?code=advanced/cubemaps_skybox),你可以对比一下你写的。 + +尝试用不同的天空盒实验,看看它们对场景有多大影响。 + +#### 优化 + +现在我们在渲染场景中的其他物体之前渲染了天空盒。这么做没错,但是不怎么高效。如果我们先渲染了天空盒,那么我们就是在为每一个屏幕上的像素运行像素着色器,即使天空盒只有部分在显示着;fragment可以使用前置深度测试(early depth testing)简单地被丢弃,这样就节省了我们宝贵的带宽。 + +所以最后渲染天空盒就能够给我们带来轻微的性能提升。采用这种方式,深度缓冲被全部物体的深度值完全填充,所以我们只需要渲染通过前置深度测试的那部分天空的fragment就行了,而且能显著减少像素着色器的调用。问题是天空盒是个1×1×1的立方体,极有可能会渲染失败,因为极有可能通不过深度测试。简单地不用深度测试渲染它也不是解决方案,这是因为天空盒会在之后覆盖所有的场景中其他物体。我们需要耍个花招让深度缓冲相信天空盒的深度缓冲有着最大深度值1.0,如此只要有个物体存在深度测试就会失败,看似物体就在它前面了。 + +在坐标系教程中我们说过,透视除法(perspective division)是在顶点着色器运行之后执行的,把gl_Position的xyz坐标除以w元素。我们从深度测试教程了解到除法结果的z元素等于顶点的深度值。利用这个信息,我们可以把输出位置的z元素设置为它的w元素,这样就会导致z元素等于1.0了,因为,当透视除法应用后,它的z元素转换为w/w = 1.0: + +```c++ +void main() +{ + vec4 pos = projection * view * vec4(position, 1.0); + gl_Position = pos.xyww; + TexCoords = position; +} +``` + +最终,标准化设备坐标就总会有个与1.0相等的z值了,1.0就是深度值的最大值。只有在没有任何物体可见的情况下天空盒才会被渲染(只有通过深度测试才渲染,否则假如有任何物体存在,就不会被渲染,只去渲染物体)。 + +我们必须改变一下深度方程,把它设置为GL_LEQUAL,原来默认的是GL_LESS。深度缓冲会为天空盒用1.0这个值填充深度缓冲,所以我们需要保证天空盒是使用小雨等于深度缓冲来通过深度测试的,而不是小于。 + +你可以在这里找到优化过的版本的源码。 + +#### 环境映射 + +我们现在有了一个把整个环境映射到为一个单独纹理的对象,我们利用这个信息能做的不仅是天空盒。使用带有场景环境的cubemap,我们还可以让物体有一个反射或折射属性。像这样使用了环境cubemap的技术叫做环境贴图技术,其中最重要的两个是反射和折射。 + +##### 反射 + +凡是是一个物体(或物体的某部分)反射他周围的环境的属性,比如物体的颜色多少有些等于它周围的环境,这要基于观察者的角度。例如一个镜子是一个反射物体:它会基于观察者的角度泛着它周围的环境。 + +反射的基本思路不难。下麦呢的图片展示了我们如何计算反射向量,然后使用这个向量去从一个cubemap中采样: + +![](http://learnopengl.com/img/advanced/cubemaps_reflection_theory.png) + +我们基于观察方向向量I和物体的法线向量N计算出反射向量R。我们可以使用GLSL的内建函数reflect来计算这个反射向量。最后向量R作为一个方向向量对cubemap进行索引/采样,返回一个环境的颜色值。最后的效果看起来就像物体反射了天空盒。 + +因为我们在场景中已经设置了一个天空盒,创建反射就不难了。我们改变一下箱子使用的那个像素着色器,给箱子一个反射属性: + +```c++ +#version 330 core +in vec3 Normal; +in vec3 Position; +out vec4 color; + +uniform vec3 cameraPos; +uniform samplerCube skybox; + +void main() +{ + vec3 I = normalize(Position - cameraPos); + vec3 R = reflect(I, normalize(Normal)); + color = texture(skybox, R); +} +``` + +我们先来计算观察/摄像机方向向量I,然后使用它来计算反射向量R,接着我们用R从天空盒cubemap采样。要注意的是,我们有了fragment的插值Normal和Position变量,所以我们需要修正顶点着色器适应它。 + +```c++ +#version 330 core +layout (location = 0) in vec3 position; +layout (location = 1) in vec3 normal; + +out vec3 Normal; +out vec3 Position; + +uniform mat4 model; +uniform mat4 view; +uniform mat4 projection; + +void main() +{ + gl_Position = projection * view * model * vec4(position, 1.0f); + Normal = mat3(transpose(inverse(model))) * normal; + Position = vec3(model * vec4(position, 1.0f)); +} +``` + +我们用了法线向量,所以我们打算使用一个法线矩阵(normal matrix)变换它们。Position输出的向量是一个世界空间位置向量。顶点着色器输出的Position用来在像素着色器计算观察方向向量。 + +因为我们使用法线,你还得更新顶点数据,更新属性指针。还要确保设置cameraPos的uniform。 + +然后在渲染箱子前我们还得绑定cubemap纹理: + +```c++ +glBindVertexArray(cubeVAO); +glBindTexture(GL_TEXTURE_CUBE_MAP, skyboxTexture); +glDrawArrays(GL_TRIANGLES, 0, 36); +glBindVertexArray(0); +``` + +编译运行你的代码,你等得到一个镜子一样的箱子。箱子完美地反射了周围的天空盒: + +![](http://learnopengl.com/img/advanced/cubemaps_reflection.png) + +你可以[从这里找到全部源代码](http://learnopengl.com/code_viewer.php?code=advanced/cubemaps_reflection)。 + +当反射应用于整个物体之上的时候,物体看上去就像有一个像钢和铬这种高反射材质。如果我们加载模型加载教程中的纳米铠甲模型,我们就会获得一个铬金属制铠甲: + +![](http://learnopengl.com/img/advanced/cubemaps_reflection_nanosuit.png) + +看起来挺惊艳,但是现实中大多数模型都不是完全反射的。我们可以引进反射贴图(reflection map)来使模型有另一层细节。和diffuse、specular贴图一样,我们可以从反射贴图上采样来决定fragment的反射率。使用反射贴图我们还可以决定模型的哪个部分有反射能力,以及强度是多少。本节的练习中,要由你来在我们早期创建的模型加载器引入反射贴图,这回极大的提升纳米服模型的细节。 + +#### 折射 + +环境映射的另一个形式叫做折射,它和反射差不多。折射是光线通过特定材质对光线方向的改变。我们通常看到像水一样的表面,光线并不是直接通过的,而是让光线弯曲了一点。它看起来像当你把半只手伸进水里的效果。 + +折射遵守[斯涅尔定律](http://en.wikipedia.org/wiki/Snell%27s_law),使用环境贴图看起来就像这样: + +![](http://learnopengl.com/img/advanced/cubemaps_refraction_theory.png) + +我们有个观察向量I,一个法线向量N,这次折射向量是R。就像你所看到的那样,观察向量的方向有轻微弯曲。弯曲的向量R随后用来从cubemap上采样。 + +折射可以通过GLSL的内建函数refract来实现,除此之外还需要一个法线向量,一个观察方向和一个两种材质之间的折射指数。 + +折射指数决定了一个材质上光线扭曲的数量,每个材质都有自己的折射指数。下表是常见的折射指数: + +材质 | 折射指数 +---|--- +空气 | 1.00 +水 | 1.33 +冰 | 1.309 +玻璃 | 1.52 +宝石 | 2.42 + +我们使用这些折射指数来计算光线通过两个材质的比率。在我们的例子中,光线/视线从空气进入玻璃(如果我们假设箱子是玻璃做的)所以比率是1.001.52 = 0.658。 + +我们已经绑定了cubemap,提供了定点数据,设置了摄像机位置的uniform。现在只需要改变像素着色器: + +```c++ +void main() +{ + float ratio = 1.00 / 1.52; + vec3 I = normalize(Position - cameraPos); + vec3 R = refract(I, normalize(Normal), ratio); + color = texture(skybox, R); +} +``` + +通过改变折射指数你可以创建出完全不同的视觉效果。编译运行应用,结果也不是太有趣,因为我们只是用了一个普通箱子,这不能显示出折射的效果,看起来像个放大镜。使用同一个着色器,纳米服模型却可以展示出我们期待的效果:玻璃制物体。 + +```c++ +void main() +{ + float ratio = 1.00 / 1.52; + vec3 I = normalize(Position - cameraPos); + vec3 R = refract(I, normalize(Normal), ratio); + color = texture(skybox, R); +} +``` + +你可以向想象一下,如果将光线、反射、折射和顶点的移动合理的结合起来就能创造出漂亮的水的图像。一定要注意,出于物理精确的考虑当光线离开物体的时候还要再次进行折射;现在我们简单的使用了单边(一次)折射,大多数目的都可以得到满足。 + +#### 动态环境贴图(Dynamic environment maps) + +现在,我们已经使用了静态图像组合的天空盒,看起来不错,但是没有考虑到物体可能移动的实际场景。我们到现在还没注意到这点,是因为我们目前还只使用了一个物体。如果我们有个镜子一样的物体,它周围有多个物体,只有天空盒在镜子中可见,和场景中只有这一个物体一样。 + +使用帧缓冲可以为提到的物体的所有6个不同角度创建一个场景的纹理,把它们每次渲染迭代储存为一个cubemap。之后我们可以使用这个(动态生成的)cubemap来创建真实的反射和折射表面,这样就能包含所有其他物体了。这种方法叫做动态环境映射(dynamic environment mapping),因为我们动态地创建了一个物体的以其四周为参考的cubemap,并把它用作环境贴图。 + +它看起效果很好,但是有一个劣势:使用环境贴图我们必须为每个物体渲染场景6次,这需要非常大的开销。现代应用尝试尽量使用天空盒子,凡可能预编译cubemap就创建少量动态环境贴图。动态环境映射是个非常棒的技术,要想在不降低执行效率的情况下实现它就需要很多巧妙的技巧。 + + + +### 练习 + +//TODO \ No newline at end of file diff --git a/04 Advanced OpenGL/07 Advanced Data.md b/04 Advanced OpenGL/07 Advanced Data.md new file mode 100644 index 0000000..60905e4 --- /dev/null +++ b/04 Advanced OpenGL/07 Advanced Data.md @@ -0,0 +1,101 @@ +## 高级数据 + +本文作者JoeyDeVries,由Django翻译自http://learnopengl.com + +我们在OpenGL中大量使用缓冲来储存数据已经有一会儿了。有一些有趣的方式来操纵缓冲,也有一些有趣的方式通过纹理来向着色器传递大量数据。本教程中,我们会讨论一些更加有意思的缓冲函数,以及如何使用纹理对象来储存大量数据(教程中纹理部分还没写)。 + +OpenGL中缓冲知识一个管理一块儿内存区域的对象,除此没有更多点的了。当把缓冲绑定到一个特定缓冲对象的时候,我们就给缓冲赋予了一个特殊的意义。当我们绑定到GL_ARRAY_BUFFER的时候,这个缓冲就是一个顶点数组缓冲,我们也可以简单地绑定到GL_ELEMENT_ARRAY_BUFFER。OpenGL内部为每个目标(target)储存一个缓冲,基于目标来处理不同的缓冲。 + +到目前为止,我们已经使用glBufferData函数填充了缓冲对象管理的内存,这个函数分类了一块内存空间,然后把数据存入内存。如果我们向它的数据(data)这个参数传递的是NULL,只会分类内存,而不会填充它。如果我们先打算开辟一些内存,稍后回到这个缓冲一点一点的填充内存的时候很有用。 + +我们还可以调用glBufferSubData函数填充特定区域的缓冲,而不是一次填充整个缓冲。这个函数需要一个缓冲目标(target),一个偏移量(offset),数据的大小以及数据本身作为传参数。这个函数新的功能是我们可以给它一个偏移量(offset)来指定我们打算填充缓冲的位置与起始位置之间的偏移量。这样我们就可以插入/更新指定区域的缓冲内存空间了。一定要记住缓冲要有足够的内存分配,所以在调用glBufferSubData之前,调用glBufferData是必须的。 + +```c++ +glBufferSubData(GL_ARRAY_BUFFER, 24, sizeof(data), &data); // 范围: [24, 24 + sizeof(data)] +``` + +把数据传进缓冲另一个方式是像缓冲内存请求一个指针,你自己直接把数据复制到缓冲中。调用glMapBuffer函数OpenGL会返回一个当前绑定缓冲的内存的地址,供我们操作: + +```c++ +float data[] = { + 0.5f, 1.0f, -0.35f + ... +}; + +glBindBuffer(GL_ARRAY_BUFFER, buffer); +// Get pointer +void* ptr = glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY); +// Now copy data into memory +memcpy(ptr, data, sizeof(data)); +// Make sure to tell OpenGL we're done with the pointer +glUnmapBuffer(GL_ARRAY_BUFFER); +``` + +调用glUnmapBuffer函数可以告诉OpenGL我们已经用完指针了,OpenGL会知道你已经做完了。通过解映射(unmapping),指针会不再可用,如果OpenGL可以把你的数据映射到缓冲上,就会返回GL_TRUE。 + +把数据直接映射到还冲上使用glMapBuffer很有用,因为不用把它储存在临时内存里。你可以从文件读取数据然后直接复制到缓冲的内存里。 + +### 分批处理顶点属性 + +使用glVertexAttribPointer函数我们可以指定缓冲内容的顶点数组的属性layout(布局)。顶点数组缓冲,使我们可以交叉属性;也就是说我们把每个顶点的位置、法线、纹理坐标放在彼此挨着的地方。现在我们了解了更多的缓冲的内容,我们可以采取另一种方式了。 + +我们可以做的是把每种类型的属性的所有向量数据批量保存在一个布局,而不是交叉布局。与交叉布局123123123123不同,我们采取批量方式111122223333。 + +当从文件加载顶点数据时你通常获取一个位置数组,一个法线数组和一个纹理坐标数组。需要花点力气才能把它们结合为交叉数据。使用glBufferSubData可以简单的实现分批处理方式: + +```c++ +GLfloat positions[] = { ... }; +GLfloat normals[] = { ... }; +GLfloat tex[] = { ... }; +// Fill buffer +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是更好的选择。 + +我们还要更新顶点属性指针来反应这些改变: + +```c++ +glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), 0); +glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)(sizeof(positions))); +glVertexAttribPointer( + 2, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat), (GLvoid*)(sizeof(positions) + sizeof(normals))); +``` + +注意,stride参数等于顶点属性的大小,由于下一个顶点属性向量可以在它的后面3(或2)的元素那儿找到。 + +这是我们有了另一种设置和指定顶点属性的方式。使用哪个方式对OpenGL来说也不会有立竿见影的效果,这只是一种采用更加组织化的方式去设置顶点属性。选用哪种方式取决于你的偏好和应用类型。 + + ### 复制缓冲 + +当你的缓冲被数据填充以后,你可能打算让其他缓冲能分享这些数据或者打算把缓冲的内容复制到另一个缓冲里。glCopyBufferSubData函数让我们能够相对容易地把一个缓冲的数据复制到另一个缓冲里。函数的原型是: + +```c++ +void glCopyBufferSubData(GLenum readtarget, GLenum writetarget, GLintptr readoffset, GLintptr writeoffset, GLsizeiptr size); +``` + +readtarget和writetarget参数是复制的来源和目的的缓冲目标。例如我们可以从一个VERTEX_ARRAY_BUFFER复制到一个VERTEX_ELEMENT_ARRAY_BUFFER,各自指定源和目的的缓冲目标。当前绑定到这些缓冲目标上的缓冲会被影响到。 + +但如果我们打算读写的两个缓冲都是顶点数组缓冲(译注:GL_VERTEX_ARRAY_BUFFER)怎么办?我们不能把用一个缓冲目标用两次。处于这个理由,这是个例外,OpenGL给了我们另外两个缓冲目标叫做:GL_COPY_READ_BUFFER和GL_COPY_WRITE_BUFFER。这样我们就可以把我们选择的缓冲,用上面二者作为readtarget和writetarget的参数绑定到新的缓冲目标上了。 + +接着glCopyBufferSubData函数会从readoffset处读取的size大小的数据,写入到writetarget缓冲的writeoffset位置。下面是一个复制两个顶点数组缓冲的例子: + +```c++ +GLfloat vertexData[] = { ... }; +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缓冲绑定为新缓冲目标类型其中之一: + +```c++ +GLfloat 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更熟悉,这些新缓冲方法就变得更有用。下个教程中我们会讨论unform缓冲对象,彼时我们会充分利用glBufferSubData。 \ No newline at end of file diff --git a/04 Advanced OpenGL/08 Advanced GLSL.md b/04 Advanced OpenGL/08 Advanced GLSL.md new file mode 100644 index 0000000..e5ba828 --- /dev/null +++ b/04 Advanced OpenGL/08 Advanced GLSL.md @@ -0,0 +1,464 @@ +## 高级GLSL + +本文作者JoeyDeVries,由Django翻译自http://learnopengl.com + +这章不会向你展示什么新的功能,也不会对你的场景的视觉效果有较大提升。本章多多少少地深入探讨了一些GLSL有意思的方面,以及可能在将来能帮助你的技巧。基本来说有些不可不知的内容和功能在你去使用GLSL创建OpenGL应用的时候能让你的生活更轻松。 + +我们会讨论一些有趣的内建变量、组织着色器输入和输出的新方式以及一个叫做uniform缓冲对象的非常有用的工具。 + +### GLSL的内建变量 + +着色器是很小的,如果我们需要从当前着色器以外的别的资源里的数据,那么我们就不得不穿给它。我们学过了使用顶点属性、uniform和采样器可以实现这个目标。GLSL有几个以gl_为前缀的变量,使我们有一个额外的手段来获取和写入数据。我们已经看到了两个:gl_Position和gl_FragCoord,前一个是顶点着色器的输出向量,后一个是像素着色器的变量。 + +我们会讨论几个有趣的GLSL内建变量,并向你解释为什么它们对我们来说很有好处。注意,我们不会讨论到GLSL中所有的内建变量,因此如果你想看到所有的内建变量还是最好去看[OpenGL的wiki](http://www.opengl.org/wiki/Built-in_Variable_(GLSL)。 + +#### 顶点着色器变量 + +我们已经了解gl_Position是顶点着色器裁切空间输出的位置向量。如果你想让屏幕上渲染出东西gl_Position必须使用。否则我们什么都看不到。 + +##### gl_PointSize + +我们可以使用的另一个可用于渲染的基本图形(primitive)是GL_POINTS,使用它每个顶点作为一个基本图形,被渲染为一个点(point)。可以glPointSize函数来设置这个点的大小,但我们还可以在顶点着色器里影响点的大小。 + +GLSL有一个输出变量叫做gl_PointSize,他是一个float变量,你可以以像素的方式设置点的高度和宽度。每一个在着色器中描述每个顶点做为点的大小。 + +在着色器中影响点的大小默认是关闭的,但是如果你打算开启它,你需要开启OpenGL的GL_PROGRAM_POINT_SIZE: + +```c++ +glEnable(GL_PROGRAM_POINT_SIZE); +``` + +把点的大小设置为裁切空间的z值,这样点的大小就等于顶点距离观察者的距离,这是一种影响点的大小的方式。当顶点距离观察者更远的时候,它就会变得更大。 + +```c++ +void main() +{ + gl_Position = projection * view * model * vec4(position, 1.0f); + gl_PointSize = gl_Position.z; +} +``` + +结果是我们绘制的点距离我们越远就越大: + +![](http://bullteacher.com/wp-content/uploads/2015/06/advanced_glsl_pointsize.png) + +想象一下,每个顶点表示出来的点的大小的不同,如果用在像粒子生成之类的技术里会挺有意思的。 + + ##### gl_VertexID + +gl_Position和gl_PointSize都是输出变量,因为它们的值是作为顶点着色器的输出被读取的;我们可以向它们写入数据来影响结果。顶点着色器为我们提供了一个有趣的输入变量,我们只能从它那里读取,这个变量叫做gl_VertexID。 + +gl_VertexID是个整型变量,它储存着我们绘制的当前顶点的ID。当进行索引渲染(indexed rendering,使用glDrawElements)时,这个变量保存着当前绘制的顶点的索引。当用的不是索引绘制(glDrawArrays)时,这个变量保存的是从渲染开始起直到当前处理的这个顶点的(当前顶点)编号。 + +尽管目前看似没用,但是我们最好知道我们能获取这样的信息。 + +#### 像素着色器的变量 + +在像素着色器中也有一些有趣的变量。GLSL给我们提供了两个有意思的输入变量,它们是gl_FragCoord和gl_FrontFacing。 + +##### gl_FragCoord + +在讨论深度测试的时候,我们已经看过gl_FragCoord好几次了,因为gl_FragCoord向量的z元素和特定的fragment的深度值相等。然而,我们也可以使用这个向量的x和y元素来实现一些有趣的效果。 + +gl_FragCoord的x和y元素是这个fragment窗口空间坐标(window-space coordinate)。它们的起始处是窗口的左下角。如果我们的窗口是800×600的,那么一个fragment的窗口空间坐标x的范围就在0到800之间,y在0到600之间。 + +我们可以使用像素着色器基于fragment的窗口坐标计算出一个不同的颜色。gl_FragCoord变量的一个常用的方式是与一个不同的fragment计算出来的视频输出进行对比,通常在技术演示中常见。比如我们可以把屏幕分为两个部分,窗口的左侧渲染一个输出,窗口的右边渲染另一个输出。下面是一个基于fragment的窗口坐标的位置的不同输出不同的颜色的像素着色器: + + +```c++ +void main() +{ + if(gl_FragCoord.x < 400) + color = vec4(1.0f, 0.0f, 0.0f, 1.0f); + else + color = vec4(0.0f, 1.0f, 0.0f, 1.0f); +} +``` + +因为窗口的宽是800,当一个像素的x坐标小于400,那么它一定在窗口的左边,这样我们就让物体有个不同的颜色。 + +![](http://bullteacher.com/wp-content/uploads/2015/06/advanced_glsl_fragcoord.png) + +我们现在可以计算出两个完全不同的像素着色器结果,每个显示在窗口的一端。这对于测试不同的光照技术很有好处。 + + + +gl_FrontFacing + +像素着色器另一个有意思的输入变量是gl_FrontFacing变量。在面剔除教程中,我们提到过OpenGL可以根据顶点绘制顺序弄清楚一个面是正面还是背面。如果我们不适用面剔除,那么gl_FrontFacing变量能告诉我们当前fragment是一个正面的一部分还是背面的一部分。然后我们可以决定做一些事情,比如为正面计算出不同的颜色。 + +gl_FrontFacing变量是一个布尔值,如果fragment是正面的一部分那么就是true,否则就是false。这样我们可以创建一个立方体,里面和外面使用不同的纹理: + +``c++ +#version 330 core +out vec4 color; +in vec2 TexCoords; + +uniform sampler2D frontTexture; +uniform sampler2D backTexture; + +void main() +{ + if(gl_FrontFacing) + color = texture(frontTexture, TexCoords); + else + color = texture(backTexture, TexCoords); +} +``` + +如果我们从箱子的一角往里看,就能看到里面用的是另一个纹理。 + +![](http://bullteacher.com/wp-content/uploads/2015/06/advanced_glsl_frontfacing.png) + +注意,如果你开启了面剔除,你就看不到箱子里面有任何东西了,所以再使用gl_FrontFacing毫无意义。 + +##### gl_FragDepth + +输入变量gl_FragCoord让我们可以读得当前fragment的窗口空间坐标和深度值,但是它是只读的。我们不能影响到这个fragment的窗口屏幕坐标,但是可以设置这个像素的深度值。GLSL给我们提供了一个叫做gl_FragDepth的变量,我们可以用它在着色器中遂舍之像素的深度值。 + +为了在着色器中设置深度值,我们简单的写一个0.0到1.0之间的float数,赋值给这个输出变量: + +```c++ +gl_FragDepth = 0.0f; // This fragment now has a depth value of 0.0f +``` + +如果着色器中没有像gl_FragDepth变量写入什么,它就会自动采用gl_FragCoord.z的值。 + +我们自己设置深度值有一个显著缺点,因为只要我们在像素着色器中对gl_FragDepth写入什么,OpenGL就会关闭所有的前置深度测试。它被关闭的原因是,在我们运行像素着色器之前OpenGL搞不清像素的深度值,因为像素着色器可能会完全改变这个深度值。 + +你也需要考虑到gl_FragDepth写入所带来的性能的下降。然而从OpenGL4.2起,我们仍然可以对二者进行一定的调和,这需要在像素着色器的顶部使用深度条件(depth condition)来重新声明gl_FragDepth: + +```c++ +layout (depth_) out float gl_FragDepth; +``` + +condition可以使用下面的值: + +Condition | 描述 + ---|--- +any | 默认值. 前置深度测试是关闭的,你失去了很多性能表现 +greater |深度值只能比gl_FragCoord.z大 +less |深度值只能设置得比gl_FragCoord.z小 +unchanged |如果写入gl_FragDepth, 你就会写gl_FragCoord.z + +如果把深度条件定义为greater或less,OpenGL会假定你只写入比当前的像素深度值的深度值大或小的。 + +下面是一个在像素着色器里增加深度值的例子,不过仍可开启前置深度测试: + +```c++ +#version 330 core +layout (depth_greater) out float gl_FragDepth; +out vec4 color; + +void main() +{ + color = vec4(1.0f); + gl_FragDepth = gl_FragCoord.z + 0.1f; +} +``` + +一定要记住这个功能只在OpenGL4.2以上版本才有。 + + + +### Interface blocks(接口块) + +到目前位置,每次我们打算从顶点向像素着色器发送数据,我们都会声明一个相互匹配的输入/输入变量。从一个着色器向另一个着色器发送数据,一次将它们声明好是最简单的方式,但是随着应用变得越来越大,你也许会打算发送的不仅仅是变量,最好还可以包括数组和结构体。 + +为了帮助我们组织这些变量,GLSL为我们提供了一些叫做interface blocks的东西,好让我们能够组织这些变量。声明interface block和声明struct有点像,不同之处是它现在基于块(block),使用in和out关键字来声明,最后它将成为一个输入或输出块(block)。 + +```c++ +#version 330 core +layout (location = 0) in vec3 position; +layout (location = 1) in vec2 texCoords; + +uniform mat4 model; +uniform mat4 view; +uniform mat4 projection; + +out VS_OUT +{ + vec2 TexCoords; +} vs_out; + +void main() +{ + gl_Position = projection * view * model * vec4(position, 1.0f); + vs_out.TexCoords = texCoords; +} +``` + +这次我们声明一个叫做vs_out的interface block,它把我们需要发送给下个阶段着色器的所有输出变量组合起来。虽然这是一个微不足道的例子,但是你可以想象一下,它的确能够帮助我们组织着色器的输入和输出。当我们希望把着色器的输入和输出组织成数组的时候它就变得很有用,我们会在下节几何着色器(geometry)中见到。 + +然后,我们还需要在下一个着色器——像素着色器中声明一个输入interface block。块名(block name)应该是一样的,但是实例名可以是任意的。 + +```c++ +#version 330 core +out vec4 color; + +in VS_OUT +{ + vec2 TexCoords; +} fs_in; + +uniform sampler2D texture; + +void main() +{ + color = texture(texture, fs_in.TexCoords); +} +``` + +如果两个interface block名一致,它们对应的输入和输出就会匹配起来。这是另一个可以帮助我们组织代码的有用功能,特别是在跨着色阶段的情况,比如几何着色器。 + +### uniform缓冲对象 + +我们使用OpenGL很长时间了,也学到了一些很酷的技巧,但是产生了一些烦恼。比如说,当时用一个以上的着色器的时候,我们必须一次次设置uniform变量,尽管对于每个着色器来说它们都是一样的,所以为什么还麻烦地多次设置它们呢? + +OpenGL为我们提供了一个叫做uniform缓冲对象的工具,使我们能够声明一系列的全局uniform变量, 它们会在几个着色器程序中保持一致。当时用uniform缓冲的对象时相关的uniform只能设置一次。我们仍需为每个着色器手工设置唯一的uniform。创建和配置一个uniform缓冲对象需要费点功夫。 + +因为uniform缓冲对象是一个缓冲,因此我们可以使用glGenBuffers创建一个,然后绑定到GL_UNIFORM_BUFFER缓冲目标上,然后把所有相关uniform数据存入缓冲。有一些原则,像uniform缓冲对象如何储存数据,我们会在稍后讨论。首先我们我们在一个简单的顶点着色器中,用uniform block(uniform块)储存projection和view矩阵: + +```c++ +#version 330 core +layout (location = 0) in vec3 position; + +layout (std140) uniform Matrices +{ + mat4 projection; + mat4 view; +}; + +uniform mat4 model; + +void main() +{ + gl_Position = projection * view * model * vec4(position, 1.0); +} +``` + +前面,大多数例子里我们在每次渲染迭代,都为projection和view矩阵设置uniform。这个例子里使用了uniform缓冲对象,这非常有用,因为这些矩阵我们设置一次就行了。 + +在这里我们声明了一个叫做Matrices的uniform block,它储存两个4×4矩阵。在uniform block中的变量可以直接获取,而不用使用block名作为前缀。接着我们在缓冲中储存这些矩阵的值,每个声明了这个uniform block的着色器已经能够获取矩阵了。 + +现在你可能会奇怪layout(std140)是什么意思。它的意思是说当前定义的uniform block为它的内容使用特定的内存布局;这个声明实际上是设置uniform block layout(uniform块布局)。 + + + +#### uniform block layout(uniform块布局) + +一个uniform block的内容更被储存到一个缓冲对象中,实际上就是在一块内存中。因为这块内存也不清楚它保存着什么类型的数据,我们就必须告诉OpenGL哪一块内存对应着色器中哪一个uniform变量。 + +假想下面的uniform block在一个着色器中: + +```c++ +layout (std140) uniform ExampleBlock +{ + float value; + vec3 vector; + mat4 matrix; + float values[3]; + bool boolean; + int integer; +}; +``` + +我们所希望知道的是每个变量的大小(以字节为单位)和偏移量(从block的起始处),所以我们可以以各自的顺序把它们放进一个缓冲里。每个元素的大小在OpenGL中都很清楚,直接与C++数据类型呼应;向量和矩阵是一个float序列(数组)。OpenGL没有澄清的是变量之间的间距。这让硬件能以它认为合适的位置方式变量。比如有些硬件可以在float旁边放置一个vec3。不是所有硬件都能这样做,在vec3旁边附加一个float之前,给vec3加一个边距使之成为4个(空间连续的)float数组。功能很好,但对于我们来说用起来不方便。 + +GLSL 默认使用的uniform内存布局叫做shared(共享)布局,叫共享是因为一旦偏移量被硬件定义,它们就会持续地被多个程序所共享。使用共享布局,GLSL可以为了优化而重新放置uniform变量,只要变量的顺序保持完整。因为我们不知道每个uniform变量的偏移量是多少,所以我们也就不知道如何精确地填充uniform缓冲。我们可以使用像glGetUniformIndices这样的函数来查询这个信息,但是这超出了本节教程的范围。 + +由于共享布局给我们做了一些空间优化。通常在实践中并不适用分享布局,而是使用std140布局。std140通过一系列的规则的规范声明了它们各自的偏移量,std140布局为每个变量类型显式地声明了内存的布局。由于被显式的提及,我们就可以手工算出每个变量的偏移量。 + +每个变量都有一个base alignment(基线对齐),它等于在一个uniform block中这个变量所占的空间(包含边距),这个基线对齐是使用std140布局原则计算出来的。然后,我们为每个变量计算出它的aligned offset(对齐偏移),这是一个变量从块(block)开始出的字节偏移量。变量对齐的字节偏移一定等于它的基线对齐的倍数。 + +准确的布局规则可以在OpenGL的uniform缓冲规范中得到,但我们会列出最常见的规范。GLSL中每个变量类型比如int、float和bool被定义为4字节的数量,每4字节被表示为N。 + +Type | Layout rule + ---|--- +Scalar e.g. int or bool | Each scalar has a base alignment of N. +Vector | Either 2N or 4N. This means that a vec3 has a base alignment of 4N. +Array of scalars or vectors | Each element has a base alignment equal to that of a vec4. +Matrices | Stored as a large array of column vectors, where each of those vectors has a base alignment of vec4. +Struct | Equal to the computed size of its elements according to the previous rules, but padded to a multiple of the size of a vec4. + +像OpenGL大多数规范一样,举个例子就很容易理解。再次利用之前介绍的uniform块ExampleBlock,我们用std140布局,计算它的每个成员的aligned offset(对齐偏移): + +```c++ +layout (std140) uniform ExampleBlock +{ + // // base alignment // aligned offset + float value; // 4 // 0 + vec3 vector; // 16 // 16 (must be multiple of 16 so 4->16) + mat4 matrix; // 16 // 32 (column 0) + // 16 // 48 (column 1) + // 16 // 64 (column 2) + // 16 // 80 (column 3) + float values[3]; // 16 // 96 (values[0]) + // 16 // 112 (values[1]) + // 16 // 128 (values[2]) + bool boolean; // 4 // 144 + int integer; // 4 // 148 +}; +``` + +尝试自己计算出偏移量,把它们和表格对比,你可以把这件事当作一个练习。使用计算出来的偏移量,根据std140布局规则,我们可以用glBufferSubData这样的函数,使用变量数据填充缓冲。虽然不是很高效,但std140布局可以保证在每个程序中声明的这个uniform块的布局保持一致。 + +在定义uniform块前面添加layout (std140)声明,我们就能告诉OpenGL这个uniform块使用了std140布局。另外还有两种其他的布局可以选择,它们需要我们在填充缓冲之前查询每个偏移量。我们已经了解了分享布局(shared layout)和其他的布局都将被封装(packed)。当使用封装(packed)布局的时候,不能保证布局在别的程序中能够保持一致,因为它允许编译器从uniform block块中优化出去uniform变量,这在每个着色器中都可能不同。 + +#### 使用uniform缓冲 + +我们讨论了uniform block在着色器中的定义和如何定义它们的内存布局,但是我们还没有讨论如何使用它们。 + +首先我们需要创建一个uniform缓冲对象,这要使用glGenBuffers来完成。当我们拥有了一个缓冲对象,我们就把它绑定到GL_UNIFORM_BUFFER目标上,调用glBufferData来给它分配足够的空间。 + +```c++ +GLuint uboExampleBlock; +glGenBuffers(1, &uboExampleBlock); +glBindBuffer(GL_UNIFORM_BUFFER, uboExampleBlock); +glBufferData(GL_UNIFORM_BUFFER, 150, NULL, GL_STATIC_DRAW); // allocate 150 bytes of memory +glBindBuffer(GL_UNIFORM_BUFFER, 0); +``` + +现在任何时候当我们打算往缓冲中更新或插入数据,我们就绑定到uboExampleBlock上,并使用glBufferSubData来更新它的内存。我们只需要更新这个uniform缓冲一次,所有的使用这个缓冲着色器就都会使用它更新的数据了。但是,OpenGL是如何知道哪个uniform缓冲对应哪个uniform block呢? + +在OpenGL环境(context)中,定义了若干绑定点(binding points),在哪儿我们可以把一个uniform缓冲链接上去。当我们创建了一个uniform缓冲,我们把它链接到一个这个绑定点上,我们也把着色器中uniform block链接到同一个绑定点上,这样就把它们链接到一起了。下面的图标表示了这点: + +![](http://bullteacher.com/wp-content/uploads/2015/06/advanced_glsl_binding_points.png) + +你可以看到,我们可以将多个uniform缓冲绑定都不同绑定点上。因为着色器A和着色器B都有一个链接到同一个绑定点0的uniform block,它们的uniform block分享同样的uniform数据——uboMatrices;有一个前提条件是两个着色器必须都定义了Matrices这个uniform block。 + +我们调用glUniformBlockBinding函数来把uniform block设置到一个特定的绑定点上。函数的第一个参数是一个程序对象,接着是一个uniform block索引(uniform block index)和打算链接的绑定点。uniform block索引是一个着色器中定义的uniform block的索引位置,可以调用glGetUniformBlockIndex来获取这个值,这个函数接收一个程序对象和uniform block的名字。我们可以从图表设置Lights这个uniform block链接到绑定点2: + +```c++ +GLuint lights_index = glGetUniformBlockIndex(shaderA.Program, "Lights"); +glUniformBlockBinding(shaderA.Program, lights_index, 2); +``` + +注意,我们必须在每个着色器中重复做这件事。 + +从OpenGL4.2起,也可以在着色器中通过添加另一个布局标识符来储存一个uniform block的绑定点,就不用我们调用glGetUniformBlockIndex和glUniformBlockBinding了。下面的代表显式设置了Lights这个uniform Block的绑定点: + + +```c++ + +layout(std140, binding = 2) uniform Lights { ... }; + +``` + +然后我们还需要把uniform缓冲对象绑定到同样的绑定点上,这个可以使用glBindBufferBase或glBindBufferRange来完成。 + +```c++ +glBindBufferBase(GL_UNIFORM_BUFFER, 2, uboExampleBlock); +// or +glBindBufferRange(GL_UNIFORM_BUFFER, 2, uboExampleBlock, 0, 150); +``` + +函数glBindBufferBase接收一个目标、一个绑定点索引和一个uniform缓冲对象作为它的参数。这个函数把uboExampleBlock链接到绑定点2上面,自此绑定点所链接的两端都链接在一起了。你还可以使用glBindBufferRange函数,这个函数还需要一个偏移量和大小作为参数,这样你就可以只把一定范围的uniform缓冲绑定到一个绑定点上了。使用glBindBufferRage函数,你能够将多个不同的uniform block链接到同一个uniform缓冲对象上。 + +现在所有事情都做好了,我们可以开始向uniform缓冲添加数据了。我们可以使用glBufferSubData将所有数据添加为一个单独的字节数组或者更新缓冲的部分内容,只要我们愿意。为了更新uniform变量boolean,我们可以这样更新uniform缓冲对象: + +```c++ +glBindBuffer(GL_UNIFORM_BUFFER, uboExampleBlock); +GLint b = true; // bools in GLSL are represented as 4 bytes, so we store it in an integer +glBufferSubData(GL_UNIFORM_BUFFER, 142, 4, &b); +glBindBuffer(GL_UNIFORM_BUFFER, 0); +``` + +同样的处理也能够应用到uniform block中其他uniform变量上。 + +#### 一个简单的例子 + +我们来师范一个真实的使用uniform缓冲对象的例子。如果我们回头看看前面所有演示的代码,我们一直使用了3个矩阵:投影、视图和模型矩阵。所有这些矩阵中,只有模型矩阵是频繁变化的。如果我们有多个着色器使用了这些矩阵,我们可能最好还是使用uniform缓冲对象。 +我们将把投影和视图矩阵储存到一个uniform block中,它被取名为Matrices。我们不打算储存模型矩阵,因为模型矩阵会频繁在着色器间更改,所以使用uniform缓冲对象真的不会带来什么好处。 + +```c++ +#version 330 core +layout (location = 0) in vec3 position; + +layout (std140) uniform Matrices +{ + mat4 projection; + mat4 view; +}; +uniform mat4 model; + +void main() +{ + gl_Position = projection * view * model * vec4(position, 1.0); +} +``` + +这儿没什么特别的,除了我们现在使用了一个带有std140布局的uniform block。我们在例程中将显示4个立方体,每个立方体都使用一个不同的着色器程序。4个着色器程序使用同样的顶点着色器,但是它们将使用各自的像素着色器,每个像素着色器输出一个单色。 + +首先,我们把顶点着色器的uniform block设置为绑定点0。注意,我们必须为每个着色器做这件事。 + +```c++ +GLuint uniformBlockIndexRed = glGetUniformBlockIndex(shaderRed.Program, "Matrices"); +GLuint uniformBlockIndexGreen = glGetUniformBlockIndex(shaderGreen.Program, "Matrices"); +GLuint uniformBlockIndexBlue = glGetUniformBlockIndex(shaderBlue.Program, "Matrices"); +GLuint uniformBlockIndexYellow = glGetUniformBlockIndex(shaderYellow.Program, "Matrices"); + +glUniformBlockBinding(shaderRed.Program, uniformBlockIndexRed, 0); +glUniformBlockBinding(shaderGreen.Program, uniformBlockIndexGreen, 0); +glUniformBlockBinding(shaderBlue.Program, uniformBlockIndexBlue, 0); +glUniformBlockBinding(shaderYellow.Program, uniformBlockIndexYellow, 0); +``` + +然后,我们创建真正的uniform缓冲对象,并把缓冲绑定到绑定点0: + +```c++ +GLuint 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的2倍。GLM的矩阵类型的大小直接对应于GLSL的mat4。然后我们把一个特定范围的缓冲链接到绑定点0,这个例子中应该是整个缓冲。 + +现在所有要做的事只剩下填充缓冲了。如果我们把视野( field of view)值保持为恒定的投影矩阵(这样就不会有摄像机缩放),我们只要在程序中定义它一次就行了,这也意味着我们只需向缓冲中把它插入一次。因为我们已经在缓冲对象中分配了足够的内存,我们可以在我们进入游戏循环之前使用glBufferSubData来储存投影矩阵: + +```c++ +glm::mat4 projection = glm::perspective(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缓冲的前半部分。在我们在每次渲染迭代绘制物体前,我们用视图矩阵更新缓冲的第二个部分: + +```c++ +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 block的顶点着色器豆浆邦汉uboMatrices所储存的数据。所以如果我们现在使用4个不同的着色器绘制4个立方体,它们的投影和视图矩阵都是一样的: + +```c++ +glBindVertexArray(cubeVAO); +shaderRed.Use(); +glm::mat4 model; +model = glm::translate(model, glm::vec3(-0.75f, 0.75f, 0.0f)); // Move top-left +glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model)); +glDrawArrays(GL_TRIANGLES, 0, 36); +// ... Draw Green Cube +// ... Draw Blue Cube +// ... Draw Yellow Cube +glBindVertexArray(0); +``` + +我们只需要在去设置一个model的unform即可。在一个像这样的场景中使用uniform缓冲对象在每个着色器中可以减少uniform的调用。最后效果看起来像这样: + +![](http://learnopengl.com/img/advanced/advanced_glsl_uniform_buffer_objects.png) + +通过改变模型矩阵,每个立方体都移动到窗口的一边,由于像素着色器不同,物体的颜色也不同。这是一个相对简单的场景,我们可以使用uniform缓冲对象,但是任何大型渲染程序有成百上千的活动着色程序;彼时uniform缓冲对象就会闪闪发光了。 + +你可以[在这里获得例程的完整源码](http://www.learnopengl.com/code_viewer.php?code=advanced/advanced_glsl_uniform_buffer_objects)。 + +uniform缓冲对象比单独的uniform有很多好处。第一,一次设置多个uniform比一次设置一个速度快。第二,如果你打算改变一个横跨多个着色器的uniform,在uniform缓冲中只需更改一次。最后一个好处可能不是很明显,使用uniform缓冲对象你可以在着色器中使用更多的uniform。OpenGL有一个对可使用uniform数据的数量的限制,可以用GL_MAX_VERTEX_UNIFORM_COMPONENTS来获取。当使用uniform缓冲对象中,这个限制的阈限会更高。所以无论何时,你达到了uniform的最大使用数量(比如做谷歌动画的时候),你可以使用uniform缓冲对象。 \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index b2df08e..3df4d5b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -27,6 +27,12 @@ pages: - 深度测试: '04 Advanced OpenGL/01 Depth testing.md' - 模板测试: '04 Advanced OpenGL/02 Stencil testing.md' - 混合: '04 Advanced OpenGL/03 Blending.md' + - 面剔除: '04 Advanced OpenGL/04 Face culling.md' + - 帧缓冲: '04 Advanced OpenGL/05 Framebuffers.md' + - Cubmaps: '04 Advanced OpenGL/06 Cubemaps.md' + - 高级数据: '04 Advanced OpenGL/07 Advanced Data.md' + - 高级GLSL: '04 Advanced OpenGL/08 Advanced GLSL.md' + - 高级光照(Advanced Lighting): - HDR: '05 Advanced Lighting/06 HDR.md'