1
0
mirror of https://github.com/LearnOpenGL-CN/LearnOpenGL-CN.git synced 2025-08-23 04:35:28 +08:00

convert 04/06 04/07 04/08

This commit is contained in:
Geequlim
2015-07-22 13:43:45 +08:00
parent e3cecdc088
commit 929f5c35a1
4 changed files with 965 additions and 0 deletions

View File

@@ -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<const GLchar*> 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<const GLchar*> 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立方体帖上纹理可以用立方体的位置作为纹理坐标进行采样。当一个立方体的中心位于原点(000)的时候,它的每一个位置向量也就是以原点为起点的方向向量。这个方向向量就是我们要得到的立方体某个位置的相应纹理值。出于这个理由,我们只需要提供位置向量,而无需纹理坐标。为了渲染天空盒,我们需要一组新着色器,它们不会太复杂。因为我们只有一个顶点属性,顶点着色器非常简单:
```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

View File

@@ -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。

View File

@@ -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_<condition>) out float gl_FragDepth;
```
condition可以使用下面的值
Condition | 描述
---|---
any | 默认值. 前置深度测试是关闭的,你失去了很多性能表现
greater |深度值只能比gl_FragCoord.z大
less |深度值只能设置得比gl_FragCoord.z小
unchanged |如果写入gl_FragDepth, 你就会写gl_FragCoord.z
如果把深度条件定义为greater或lessOpenGL会假定你只写入比当前的像素深度值的深度值大或小的。
下面是一个在像素着色器里增加深度值的例子,不过仍可开启前置深度测试:
```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 blockuniform块储存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 layoutuniform块布局
#### uniform block layoutuniform块布局
一个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缓冲对象。

View File

@@ -27,6 +27,12 @@ pages:
- 深度测试: '04 Advanced OpenGL/01 Depth testing.md' - 深度测试: '04 Advanced OpenGL/01 Depth testing.md'
- 模板测试: '04 Advanced OpenGL/02 Stencil testing.md' - 模板测试: '04 Advanced OpenGL/02 Stencil testing.md'
- 混合: '04 Advanced OpenGL/03 Blending.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): - 高级光照(Advanced Lighting):
- HDR: '05 Advanced Lighting/06 HDR.md' - HDR: '05 Advanced Lighting/06 HDR.md'