内容简介:3D游戏引擎的核心是渲染,游戏品质的提升需要通过Shader编程实现渲染技术,通常的渲染方式一般会通过Direct3D或者是OpenGL,对于目前比较流行的引擎Unity3D,Cocos2d-x,UE4引擎在移动端的渲染都是采用的OpenGL,所以掌握OpenGL的渲染非常重要,这有助于我们了解引擎内部的实现方式。对于Shader脚本,实现方式主要分为顶点着色器和片段着色器,顶点着色器计算得到的值是传递给片段着色器使用的,下面就详细介绍Shader编程的核心内容。每次我们打算从顶点向片段着色器发送数据,我
| 编辑推荐: |
| 本文来自于csdn,介绍了介绍Shader编程:uniform块储存投影和视图矩阵,更新或插入数据等核心内容。 |
3D游戏引擎的核心是渲染,游戏品质的提升需要通过Shader编程实现渲染技术,通常的渲染方式一般会通过Direct3D或者是OpenGL,对于目前比较流行的引擎Unity3D,Cocos2d-x,UE4引擎在移动端的渲染都是采用的OpenGL,所以掌握OpenGL的渲染非常重要,这有助于我们了解引擎内部的实现方式。
对于Shader脚本,实现方式主要分为顶点着色器和片段着色器,顶点着色器计算得到的值是传递给片段着色器使用的,下面就详细介绍Shader编程的核心内容。
每次我们打算从顶点向片段着色器发送数据,我们都会声明一个相互匹配的输出/输入变量。从一个着色器向另一个着色器发送数据,一次将它们声明好是最简单的方式,但是随着应用变得越来越大,你也许会打算发送的不仅仅是变量,最好还可以包括数组和结构体。
为了帮助我们组织这些变量,GLSL为我们提供了一些叫做接口块(Interface Blocks)的东西,好让我们能够组织这些变量。声明接口块和声明struct有点像,不同之处是它现在基于块(block),使用in和out关键字来声明,最后它将成为一个输入或输出块(block)。
#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。块名(block name)应该是一样的,但是实例名可以是任意的。
#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名一致,它们对应的输入和输出就会匹配起来。这是另一个可以帮助我们组织代码的有用功能,特别是在跨着色阶段的情况,比如几何着色器。
如果大家使用OpenGL很长时间了,也学到了一些很酷的技巧,但是产生了一些烦恼。比如说,当时用一个以上的着色器的时候,我们必须一次次设置uniform变量,尽管对于每个着色器来说它们都是一样的,所以为什么还麻烦地多次设置它们呢?
OpenGL为我们提供了一个叫做uniform缓冲对象(Uniform Buffer Object)的工具,使我们能够声明一系列的全局uniform变量, 它们会在几个着色器程序中保持一致。当时用uniform缓冲的对象时相关的uniform只能设置一次。我们仍需为每个着色器手工设置唯一的uniform。创建和配置一个uniform缓冲对象需要费点功夫。
因为uniform缓冲对象是一个缓冲,因此我们可以使用glGenBuffers创建一个,然后绑定到GL_UNIFORM_BUFFER缓冲目标上,然后把所有相关uniform数据存入缓冲。有一些原则,像uniform缓冲对象如何储存数据,我们会在稍后讨论。首先我们我们在一个简单的顶点着色器中,用uniform块(uniform block)储存投影和视图矩阵:
#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块,它储存两个4×4矩阵。在uniform块中的变量可以直接获取,而不用使用block名作为前缀。接着我们在缓冲中储存这些矩阵的值,每个声明了这个uniform块的着色器都能够获取矩阵。
现在你可能会奇怪layout(std140)是什么意思。它的意思是说当前定义的uniform块为它的内容使用特定的内存布局,这个声明实际上是设置uniform块布局(uniform block layout)。
一个uniform块的内容被储存到一个缓冲对象中,实际上就是在一块内存中。因为这块内存也不清楚它保存着什么类型的数据,我们就必须告诉OpenGL哪一块内存对应着色器中哪一个uniform变量。
假想下面的uniform块在一个着色器中:
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 layout),叫共享是因为一旦偏移量被硬件定义,它们就会持续地被多个程序所共享。使用共享布局,GLSL可以为了优化而重新放置uniform变量,只要变量的顺序保持完整。因为我们不知道每个uniform变量的偏移量是多少,所以我们也就不知道如何精确地填充uniform缓冲。我们可以使用像glGetUniformIndices这样的函数来查询这个信息,但是这超出了本节教程的范围。
由于共享布局给我们做了一些空间优化。通常在实践中并不适用分享布局,而是使用std140布局。std140通过一系列的规则的规范声明了它们各自的偏移量,std140布局为每个变量类型显式地声明了内存的布局。由于被显式的提及,我们就可以手工算出每个变量的偏移量。
每个变量都有一个基线对齐(base alignment),它等于在一个uniform块中这个变量所占的空间(包含边距),这个基线对齐是使用std140布局原则计算出来的。然后,我们为每个变量计算出它的对齐偏移(aligned offset),这是一个变量从块(block)开始处的字节偏移量。变量对齐的字节偏移一定等于它的基线对齐的倍数。
准确的布局规则可以在OpenGL的uniform缓冲规范中得到,但我们会列出最常见的规范。GLSL中每个变量类型比如int、float和bool被定义为4字节,每4字节被表示为N。
像OpenGL大多数规范一样,举个例子就很容易理解。再次利用之前介绍的uniform块ExampleBlock,我们用std140布局,计算它的每个成员的aligned offset(对齐偏移):
layout (std140) uniform ExampleBlock
{
// base alignment ---------- // aligned offset
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 (数组中的标量与vec4相同)//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块中优化出去uniform变量,这在每个着色器中都可能不同。
我们讨论了uniform块在着色器中的定义和如何定义它们的内存布局,但是我们还没有讨论如何使用它们。
首先我们需要创建一个uniform缓冲对象,这要使用glGenBuffers来完成。当我们拥有了一个缓冲对象,我们就把它绑定到GL_UNIFORM_BUFFER目标上,调用glBufferData来给它分配足够的空间。
GLuint uboExampleBlock;
glGenBuffers (1, &uboExampleBlock);
glBindBuffer (GL_UNIFORM_BUFFER, uboExampleBlock);
glBufferData (GL_UNIFORM_BUFFER, 150, NULL, GL_ STATIC_ DRAW); // 分配150个字节的内存空间
glBindBuffer (GL_UNIFORM_BUFFER, 0);
现在任何时候当我们打算往缓冲中更新或插入数据,我们就绑定到uboExampleBlock上,并使用glBufferSubData来更新它的内存。我们只需要更新这个uniform缓冲一次,所有的使用这个缓冲着色器就都会使用它更新的数据了。但是,OpenGL是如何知道哪个uniform缓冲对应哪个uniform块呢?
在OpenGL环境(context)中,定义了若干绑定点(binding points),在哪儿我们可以把一个uniform缓冲链接上去。当我们创建了一个uniform缓冲,我们把它链接到一个这个绑定点上,我们也把着色器中uniform块链接到同一个绑定点上,这样就把它们链接到一起了。下面的图标表示了这点:
你可以看到,我们可以将多个uniform缓冲绑定到不同绑定点上。因为着色器A和着色器B都有一个链接到同一个绑定点0的uniform块,它们的uniform块分享同样的uniform数据—uboMatrices有一个前提条件是两个着色器必须都定义了Matrices这个uniform块。
我们调用glUniformBlockBinding函数来把uniform块设置到一个特定的绑定点上。函数的第一个参数是一个程序对象,接着是一个uniform块索引(uniform block index)和打算链接的绑定点。uniform块索引是一个着色器中定义的uniform块的索引位置,可以调用glGetUniformBlockIndex来获取这个值,这个函数接收一个程序对象和uniform块的名字。我们可以从图表设置Lights这个uniform块链接到绑定点2:
GLuint lights_index = glGetUniformBlockIndex (shaderA . Program , "Lights");
glUniformBlockBinding (shaderA.Program, lights_index, 2 );
注意,我们必须在每个着色器中重复做这件事。
从OpenGL4.2起,也可以在着色器中通过添加另一个布局标识符来储存一个uniform块的绑定点,就不用我们调用glGetUniformBlockIndex和glUniformBlockBinding了。下面的代表显式设置了Lights这个uniform块的绑定点:
然后我们还需要把uniform缓冲对象绑定到同样的绑定点上,这个可以使用glBindBufferBase或glBindBufferRange来完成。
glBindBufferBase(GL_UNIFORM_BUFFER, 2, uboExampleBlock );
// 或者
glBindBufferRange (GL_UNIFORM_BUFFER, 2, uboExample Block , 0, 150);
函数glBindBufferBase接收一个目标、一个绑定点索引和一个uniform缓冲对象作为它的参数。这个函数把uboExampleBlock链接到绑定点2上面,自此绑定点所链接的两端都链接在一起了。你还可以使用glBindBufferRange函数,这个函数还需要一个偏移量和大小作为参数,这样你就可以只把一定范围的uniform缓冲绑定到一个绑定点上了。使用glBindBufferRage函数,你能够将多个不同的uniform块链接到同一个uniform缓冲对象上。
现在所有事情都做好了,我们可以开始向uniform缓冲添加数据了。我们可以使用glBufferSubData将所有数据添加为一个单独的字节数组或者更新缓冲的部分内容,只要我们愿意。为了更新uniform变量boolean,我们可以这样更新uniform缓冲对象:
glBindBuffer (GL_ UNIFORM_BUFFER, uboExampleBlock);
GLint b = true; // GLSL中的布尔值是4个字节,因此我们将它创建为一个4字节的整数
glBufferSubData (GL_ UNIFORM_BUFFER, 142, 4, &b);
glBindBuffer (GL_UNIFORM_BUFFER, 0);
同样的处理也能够应用到uniform块中其他uniform变量上。
下面通过一个简单的例子给读者介绍一下:
使用uniform缓冲对象的例子。如果我们回头看看前面所有演示的代码,我们一直使用了3个矩阵:投影、视图和模型矩阵。所有这些矩阵中,只有模型矩阵是频繁变化的。如果我们有多个着色器使用了这些矩阵,我们可能最好还是使用uniform缓冲对象。
我们将把投影和视图矩阵储存到一个uniform块中,它被取名为Matrices。我们不打算储存模型矩阵,因为模型矩阵会频繁在着色器间更改,所以使用uniform缓冲对象真的不会带来什么好处。
#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块。我们在例程中将显示4个立方体,每个立方体都使用一个不同的着色器程序。4个着色器程序使用同样的顶点着色器,但是它们将使用各自的片段着色器,每个片段着色器输出一个单色。
首先,我们把顶点着色器的uniform块设置为绑定点0。注意,我们必须为每个着色器做这件事。
GLuint uniformBlockIndexRed = glGetUniformBlockIndex (shaderRed .Program, "Matrices");
GLuint uniformBlockIndexGreen = glGetUniformBlockIndex (shaderGreen .Program, "Matrices");
GLuint uniformBlockIndexBlue = glGetUniformBlockIndex (shaderBlue .Program, "Matrices");
GLuint uniformBlockIndexYellow = glGetUniformBlock Index (shaderYellow.Program, "Matrices");
glUniform BlockBinding (shaderRed.Program, uniformBlock IndexRed, 0);
glUniform BlockBinding (shaderGreen.Program, uniformBlockIndexGreen, 0);
glUniformBlockBinding (shaderBlue.Program, uniformBlockIndexBlue, 0);
glUniformBlockBinding (shaderYellow.Program, uniformBlockIndexYellow, 0);
然后,我们创建真正的uniform缓冲对象,并把缓冲绑定到绑定点0:
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来储存投影矩阵:
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缓冲的前半部分。在我们在每次渲染迭代绘制物体前,我们用视图矩阵更新缓冲的第二个部分:
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 )); // 移动到左上方
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm:: value_ ptr (model));
glDrawArrays (GL_TRIANGLES, 0, 36);
// ... 绘制绿色立方体
// ... 绘制蓝色立方体
// ... 绘制黄色立方体
glBindVertexArray(0);
我们只需要在去设置一个model的uniform即可。在一个像这样的场景中使用uniform缓冲对象在每个着色器中可以减少uniform的调用。最后效果看起来像这样:
通过改变模型矩阵,每个立方体都移动到窗口的一边,由于片段着色器不同,物体的颜色也不同。这是一个相对简单的场景,我们可以使用uniform缓冲对象,但是任何大型渲染程序有成百上千的活动着色程序,彼时uniform缓冲对象就会闪闪发光了。现把核心代码给读者展示一下:
// Setup cube VAO
GLuint cubeVAO, cubeVBO;
glGenVertexArrays (1, &cubeVAO);
glGenBuffers (1, &cubeVBO);
glBindVertexArray (cubeVAO);
glBindBuffer (GL_ARRAY_BUFFER, cubeVBO);
glBufferData (GL_ ARRAY_BUFFER, sizeof(cubeVertices), & cubeVertices , GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer (0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof (GLfloat ), (GLvoid*)0);
glBindVertexArray(0);
#pragma endregion
// Create a uniform buffer object
// First . We get the relevant block indices
GLuint uniformBlockIndexRed = glGetUniformBlockIndex (shaderRed .Program , "Matrices");
GLuint uniformBlockIndexGreen = glGetUniformBlockIndex (shaderGreen .Program , "Matrices");
GLuint uniformBlockIndexBlue = glGetUniformBlockIndex (shaderBlue .Program, "Matrices");
GLuint uniformBlock IndexYellow = glGetUniformBlock Index (shaderYellow.Program, "Matrices");
// Then we link each shader's uniform block to this uniform binding point
glUniformBlockBinding(shaderRed.Program, uniformBlock IndexRed , 0);
glUniformBlockBinding (shaderGreen.Program, uniform BlockIndexGreen, 0);
glUniformBlockBinding(shaderBlue.Program, uniformBlock IndexBlue , 0);
glUniformBlockBinding(shaderYellow.Program, uniform BlockIndexYellow, 0);
// Now actually create the buffer
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);
// Define the range of the buffer that links to a uniform binding point
glBindBufferRange (GL_UNIFORM_BUFFER, 0, uboMatrices, 0, 2 * sizeof (glm::mat4));
// Store the projection matrix (we only have to do this once) (note: we're not using zoom anymore by changing the FoV. We only create the projection matrix once now)
glm::mat4 projection = glm::perspective(45.0f, (float) screenWidth /(float)screenHeight, 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);
绘制上图中展示的四个立方体核心代码如下所示:
// Clear buffers
glClearColor (0.1f, 0.1f, 0.1f, 1.0f);
glClear (GL_COLOR_ BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// Set the view and projection matrix in the uniform block - we only have to do this once per loop iteration .
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);
// Draw 4 cubes
// RED
glBindVertexArray(cubeVAO);
shaderRed.Use();
glm::mat4 model;
model = glm::translate (model, glm::vec3 (-0.75f, 0.75f , 0.0f)); // Move top-left
glUniformMatrix4fv (glGetUniformLocation (shaderRed . Program, " model "), 1, GL_ FALSE, glm ::value_ ptr (model ));
glDrawArrays (GL_ TRIANGLES, 0, 36);
// GREEN
shaderGreen.Use();
model = glm::mat4();
model = glm::translate (model, glm::vec3(0.75f, 0.75f, 0.0f )); // Move top-right
glUniformMatrix4fv (glGetUniformLocation (shaderGreen. Program, "model"), 1, GL_ FALSE, glm::value_ptr (model ));
glDrawArrays (GL_ TRIANGLES, 0, 36);
// BLUE
shaderBlue.Use();
model = glm::mat4();
model = glm::translate (model, glm::vec3 (-0.75f, -0.75f , 0.0f )); // Move bottom-left
glUniformMatrix4fv (glGetUniformLocation (shaderBlue .Program , "model "), 1, GL_FALSE, glm::value_ ptr (model ));
glDrawArrays (GL_TRIANGLES, 0, 36);
// YELLOW
shaderYellow.Use();
model = glm::mat4();
model = glm::translate (model, glm::vec3 (0.75f, - 0.75f , 0.0f )); // Move bottom-right
glUniformMatrix4fv (glGetUniformLocation (shaderYellow .Program , "model "), 1, GL_ FALSE, glm ::value_ptr ( model ));
glDrawArrays (GL_ TRIANGLES, 0, 36);
glBindVertexArray(0);
以上就是关于Shader的技术讲解,掌握一门技术要了解的知识很多的,在这里只是给读者介绍了Shader编写的一些原理,希望对读者有所帮助。。。。。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 函数式编程的核心思想
- Java 多线程编程核心技术
- Go语言核心编程(临时发布版)
- Go核心编程-面向对象 [OOP]
- 《Go语言核心编程》一书正式上市了
- Java并发编程(02):线程核心机制,基础概念扩展
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Building Social Web Applications
Gavin Bell / O'Reilly Media / 2009-10-1 / USD 34.99
Building a social web application that attracts and retains regular visitors, and gets them to interact, isn't easy to do. This book walks you through the tough questions you'll face if you're to crea......一起来看看 《Building Social Web Applications》 这本书的介绍吧!