Skip to content

Latest commit

 

History

History
631 lines (395 loc) · 30.3 KB

7.OpenGL ES着色器语言GLSL.md

File metadata and controls

631 lines (395 loc) · 30.3 KB

7.OpenGL ES着色器语言GLSL

齐次坐标(Homogeneous coordinates)

三维顶点视为三元组(x,y,z)。现在引入一个新的分量w,得到向量(x,y,z,w)。请先记住以下两点:

若w==1,则向量(x, y, z, 1)为空间中的点。 若w==0,则向量(x, y, z, 0)为方向。

齐次坐标使得我们可以用同一个公式对点和方向作运算。

着色器是使用一种叫GLSL的类C语言写成的。GLSL是为图形计算量身定制的,它包含一些针对向量和矩阵操作的有用特性。

着色器的开头总是要声明版本,接着是输入和输出变量、uniform和main函数。
每个着色器的入口点都是main函数,在这个函数中我们处理所有的输入变量,并将结果输出到输出变量中。

和其他编程语言一样,GLSL有数据类型可以来指定变量的种类。
GLSL中包含C等其它语言大部分的默认基础数据类型:int、float、double、uint和bool。
GLSL也有两种容器类型,它们会在这个教程中使用很多,分别是向量(Vector)和矩阵(Matrix)。

顶点着色器:

#version 300 es
layout (location = 0) in vec3 position; // position变量的属性位置值为0

out vec4 vertexColor; // 为片段着色器指定一个颜色输出

void main() {
    gl_Position = vec4(position, 1.0); // 注意我们如何把一个vec3作为vec4的构造器的参数
    vertexColor = vec4(0.5f, 0.0f, 0.0f, 1.0f); // 把输出变量设置为暗红色
}
#version 300 es是表示GLSL语言的版本为300,对应的是OpenGL ES 3.0版本。
#version必须写在文件的第一行,即是第一行是注释也不行。

顶点着色器的输入包括:

  • 顶点着色器输入(或者属性):用顶点数组提供的每个顶点的数据。
  • 统一变量(uniform):顶点或片段着色器使用的不变的数据。
  • 采样器:代表顶点着色器使用纹理的特殊统一变量类型。
  • 着色器程序:描述顶点上执行操作的顶点着色器的程序源代码或可执行文件。

这里需要重点讲一下上面的版本:

OpenGL ES版本有自己的着色器语言,其中OpenGL ES的版本与GLSL的版本有对应的关联关系,如果没有在着色器文件中用#version标明使用版本的时候默认使用的是OpenGL ES 2.0版本(GLSL ES 100):

  • OpenGL ES 2.0版本对应GLSL ES 100版本
  • OpenGL ES 3.0版本对应GLSL ES 300版本

我们这里使用的都是GLSL ES 300版本。但是GLSL ES 100 和 300版本中间有一些差异,这就是为什么我们有时候网上的一些代码编译时却报错的原因:

  • GLSL ES 300版本中in和out代替了之前的属性和变化(attribute和varying),

    OpenGL ES 3.0中将2.0的attribute改成了in,顶点着色器的varying改成out,片段着色器的varying改成了in,也就是说顶点着色器的输出就是片段着色器的输入,另外uniform跟2.0用法一样。

  • OpenGL ES 3.0的shader中没有texture2D()和texture3D等了,全部使用texture()方法替换。

  • 可直接使用layout制定属性值。 300版本中布局限定符可以声明顶点着色器输入和片段着色器输出的问题,例如layout(location = 2) in vec3 values[4];,如果不限定该属性的位置为2,需要通过glGetAttribuLocation()查询

  • 舍弃了gl_FragColor和gl_FragData内置属性,变成我们需要自己使用out关键字定义的属性,例如out vec4 fragColor,这就是为什么你从网还是哪个找到的代码换成GLSL ES 300版本后报错的原因。不过保留了gl_Position

  • GL_OES_EGL_image_external被废弃

    当使用samplerExternalOES是,如果在#extension GL_OES_EGL_image_external : require会报错,需要变成#extension GL_OES_EGL_image_external_essl3 : require

  • #version 300 es这种声明版本的语句,必须放到第一行,并且shader中不能有Tab键,只能用空格替换。

虽然着色器是各自独立的小程序,但是它们都是一个整体的一部分,出于这样的原因,我们希望每个着色器都有输入和输出,这样才能进行数据交流和传递。
GLSL定义了in和out关键字专门来实现这个目的。
每个着色器使用这两个关键字设定输入和输出,只要一个输出变量与下一个着色器阶段的输入匹配,它就会传递下去。
但在顶点和片段着色器中会有点不同。

in、out

顶点着色器应该接收的是一种特殊形式的输入,否则就会效率低下。
顶点着色器的输入特殊在,它从顶点数据中直接接收输入。
为了定义顶点数据该如何管理,我们使用location这一元数据指定输入变量,这样我们才可以在CPU上配置顶点属性。
我们已经在前面的教程看过这个了,layout (location = 0)。顶点着色器需要为它的输入提供一个额外的layout标识,这样我们才能把它链接到顶点数据。

你也可以忽略layout (location = 0)标识符,通过在OpenGL代码中使用glGetAttribLocation查询属性位置值(Location),但是我更喜欢在着色器中设置它们,这样会更容易理解而且节省你(和OpenGL)的工作量。

如果我们打算从一个着色器向另一个着色器发送数据,我们必须在发送方着色器中声明一个输出,在接收方着色器中声明一个类似的输入。
当类型和名字都一样的时候,OpenGL就会把两个变量链接到一起,它们之间就能发送数据了(这是在链接程序对象时完成的)。

顶点着色器:

#version 330 core
layout (location = 0) in vec3 aPos; // 位置变量的属性位置值为0

out vec4 vertexColor; // 为片段着色器指定一个颜色输出

void main()
{
    gl_Position = vec4(aPos, 1.0); // 注意我们如何把一个vec3作为vec4的构造器的参数
    vertexColor = vec4(0.5, 0.0, 0.0, 1.0); // 把输出变量设置为暗红色
}

片段着色器:

#version 330 core
out vec4 FragColor;

in vec4 vertexColor; // 从顶点着色器传来的输入变量(名称相同、类型相同)

void main()
{
    FragColor = vertexColor;
}

你可以看到我们在顶点着色器中声明了一个vertexColor变量作为vec4输出,并在片段着色器中声明了一个类似的vertexColor。
由于它们名字相同且类型相同,片段着色器中的vertexColor就和顶点着色器中的vertexColor链接了。
由于我们在顶点着色器中将颜色设置为深红色,最终的片段也是深红色的。

顶点着色器中用in限定符修饰的变量其值实质是由宿主程序(本书中为Java、C++)批量传入渲染管线的,管线进行基本处理后再传递给顶点着色器。 数据中有多少个顶点,管线就调用多少次顶点着色器,每次将一个顶点的各种属性数据传递给顶点着色器中对应的in变量。因此,顶点着色器每次执行将完成对一个顶点各项属性数据的处理。

顶点着色器每顶点执行一次,而片元着色器每片元执行一次,片元着色器的执行次数明显大于顶点着色器的执行次数。因此在开发中,应尽量减少片元着色器的运算量,可以将一些复杂运算尽量放在顶点着色器中执行。

精度限定符

精度限定符使着色器创作者可以指定着色器变量的计算精度。变量可以声明为低、中或者高精度。这些限定符用于提示编译器允许在较低的范围和精度上执行变量计算。在较低的精度上,有些OpenGL ES实现在运行着色器时可能更快,或者电源效率更高。 当然,这种效率提升是以精度为代价的,在没有正确使用精度限定符时可能造成伪像。 精度限定符可以用于指定任何基于浮点数或者整数的变量的精度。指定精度的关键字是lowp、mediump和highp。

highp vec4 position;
mediump float specularExp;

除了精度限定符之外,还有默认精度的概念。也就是说,如果变量声明时没有使用精度限定符,它将拥有该类型的默认精度。默认精度限定符在顶点或片段着色器的开头用如下语法指定:

precision highp float;
precision mediump int;

为float类型指定的精度将用作所有基于浮点值的变量的默认精度。同样,为int指定的精度将用作所有基于整数的变量的默认精度。 在顶点着色器中,如果没有指定默认精度,则int和float的默认精度都为highp,也就是说,顶点着色器中所有没有精度限定符声明的变量都使用最高的精度。

但片段着色器的规则与此不同,在片段着色器中,浮点值没有默认的精度值:每个着色器必须声明一个默认的float精度,或者为每个float变量指定精度。

Uniform

CPU在处理并行线程的时候,每个线程负责给完整图像的一部分配置颜色。
尽管每个线程和其他线程之间不能有数据交换,但我们能从CPU给每个线程输入数据。
因为显卡的架构,所有线程的输入值必须统一(uniform),而且必须设为只读。

也就是说,每条线程接收相同的数据,并且是不可改变的数据。这些输入值叫做uniform(统一值).

按业界传统应该在uniform值的名字前加u_,这样一看便知是uniform。

Uniform是一种从CPU中的应用向GPU中的着色器发送数据的方式,但uniform和顶点属性有些不同。

你可以把uniform想象成连通CPU和GPU的许多的小桥梁。

首先,uniform是全局的(Global)。
全局意味着uniform变量必须在每个着色器程序对象中都是独一无二的,而且它可以被着色器程序的任意着色器在任意阶段访问。

第二,无论你把uniform值设置成什么,uniform会一直保存它们的数据,直到它们被重置或更新。

统一变量通常保存在硬件中,这个区域被称作“常量存储”,是硬件中为存储常量值而分配的特殊空间。

因为常量存储的大小一般是固定的,所以程序中可以使用的统一变量数量受到限制。
这种限制可以通过读取内建变量gl_MaxVertexUniformVectors和gl_MaxFragmentUniformVectors的值来确定。 但是对于在编译时已知值的变量应该是常量,而不是统一变量,这样可以提高效率。

我们可以在一个着色器中添加uniform关键字至类型和变量名前来声明一个GLSL的uniform。从此处开始我们就可以在着色器中使用新声明的uniform了。我们来看看这次是否能通过uniform设置三角形的颜色:

#version 330 core
out vec4 FragColor;

uniform vec4 ourColor; // 在OpenGL程序代码中设定这个变量

void main()
{
    FragColor = ourColor;
}

我们在片段着色器中声明了一个uniform vec4的ourColor,并把片段着色器的输出颜色设置为uniform值的内容。
因为uniform是全局变量,我们可以在任何着色器中定义它们,而无需通过顶点着色器作为中介。
顶点着色器中不需要这个uniform,所以我们不用在那里定义它。

如果你声明了一个uniform却在GLSL代码中没用过,编译器会静默移除这个变量,导致最后编译出的版本中并不会包含它,这可能导致几个非常麻烦的错误,记住这点!

这个uniform现在还是空的;我们还没有给它添加任何数据,所以下面我们就做这件事。我们首先需要找到着色器中uniform属性的索引/位置值。当我们得到uniform的索引/位置值后,我们就可以更新它的值了。这次我们不去给像素传递单独一个颜色,而是让它随着时间改变颜色:

float timeValue = glfwGetTime();
float greenValue = (sin(timeValue) / 2.0f) + 0.5f;
int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");
glUseProgram(shaderProgram);
glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);

首先我们通过glfwGetTime()获取运行的秒数。然后我们使用sin函数让颜色在0.0到1.0之间改变,最后将结果储存到greenValue里。

接着,我们用glGetUniformLocation查询uniform ourColor的位置值。我们为查询函数提供着色器程序和uniform的名字(这是我们希望获得的位置值的来源)。
如果glGetUniformLocation返回-1就代表没有找到这个位置值。
最后,我们可以通过glUniform4f函数设置uniform值。注意,查询uniform地址不要求你之前使用过着色器程序,但是更新一个uniform之前你必须先使用程序(调用glUseProgram),因为它是在当前激活的着色器程序中设置uniform的。

因为OpenGL在其核心是一个C库,所以它不支持类型重载,在函数参数不同的时候就要为其定义新的函数;glUniform是一个典型例子。这个函数有一个特定的后缀,标识设定的uniform的类型。可能的后缀有:

  • f : 函数需要一个float作为它的值
  • i : 函数需要一个int作为它的值
  • ui: 函数需要一个unsigned int作为它的值
  • 3f: 函数需要3个float作为它的值
  • fv: 函数需要一个float向量/数组作为它的值

每当你打算配置一个OpenGL的选项时就可以简单地根据这些规则选择适合你的数据类型的重载函数。在我们的例子里,我们希望分别设定uniform的4个float值,所以我们通过glUniform4f传递我们的数据(注意,我们也可以使用fv版本)。

现在你知道如何设置uniform变量的值了,我们可以使用它们来渲染了。如果我们打算让颜色慢慢变化,我们就要在游戏循环的每一次迭代中(所以他会逐帧改变)更新这个uniform,否则三角形就不会改变颜色。

顶点着色器中的乘法将矩阵变换应用于顶点,将其转换为相机空间(请注意从右到左的计算顺序)。这些值被放入内置的OpenGL输出变量gl_Position中,然后继续通过管线,并由光栅着色器进行插值。 插值后的像素位置(称为片段)被发送到片段着色器(fragment shader)。回想一下,片段着色器的主要目的是设置输出像素的颜色。与顶点着色器的方式类似,片段着色器逐个处理像素,并对每个像素单独调用。

image

GLSL的特点

OpenGLES的着色器语言GLSL是一种高级的图形化编程语言,其源自应用广泛的C语言。与传统的C语言不同的是,它提供了更加丰富的针对于图像处理的原生类型,诸如向量、矩阵之类。GLSL主要包含以下特性:

  • GLSL是一种面向过程的语言,和c相同
  • GLSL的基本语法与C/C++相同
  • 它完美的支持向量和矩阵的操作
  • 它是通过限定符操作来管理输入输出类型的
  • GLSL提供了大量的内置函数来提供丰富的扩展功能

与C语言有区别的地方:

  • 共享命名空间(Shared Namespace)
    Shaders操作是相互独立的,但是为了方便Shaders间通信,在链接成一个shader program时会共享变量的名字。
  • GLSL没有char、char *和string数据类型,也没有字符串操作
  • 不支持隐式类型转换,例如: int(arg)
  • vec数据方位: vec4(r,g,b,a / x,y,z,w / s,t,p,q)。 vec3.xy = vec2
  • 新的变量类型: Attribute、Uniform、Varying

基本数据类型

GLSL中的数据类型主要分为标量、向量、矩阵、采样器、结构体、数组、空类型七种类型。如下:

  • 标量

标量表示的是只有大小没有方向的量,在GLSL中标量只有bool、int和float三种
对于int,和C一样,可以写为十进制(16)、八进制(020)或者十六进制(0x10)。
对于标量的运算,我们最需要注意的是精度,防止溢出问题。

  • 向量

向量我们可以看做是数组,在GLSL通常用于储存颜色、坐标等数据,针对维数,可分为二维、三维和四维向量。
针对存储的标量类型,可以分为bool、int和float。
共有vec2、vec3、vec4,ivec2、ivec3、ivec4、bvec2、bvec3和bvec4九种类型,数组代表维数、i表示int类型、b表示bool类型。

需要注意的是,GLSL中的向量表示竖向量,所以与矩阵相乘进行变换时,矩阵在前,向量在后(与DirectX正好相反)

向量在GPU中由硬件支持运算,比CPU快的多。

作为颜色向量时,用rgba表示分量,就如同取数组的中具体数据的索引值。三维颜色向量就用rgb表示分量。比如对于颜色向量vec4 color,color[0]和color.r都表示color向量的第一个值,也就是红色的分量。其他相同。

作为位置向量时,用xyzw表示分量,xyz分别表示xyz坐标,w表示向量的模。三维坐标向量为xyz表示分量,二维向量为xy表示分量。

作为纹理向量时,用stpq表示分量,三维用stp表示分量,二维用st表示分量。

我们大部分时候使用vecn,因为float足够满足大多数要求了。

vec2 vect = vec2(0.5, 0.7);
vec4 result = vec4(vect, 0.0, 0.0);

vec4表示矩形

通常,一个矩形可以用其左上角位置(x, y),宽度(width),高度(height)来定义。我们可以将这些值存储在一个 vec4 中,如下:

vec4 rect = vec4(x, y, width, height);

这里的 rect 向量表示:

  • rect.x 是矩形的左上角的 x 坐标。

  • rect.y 是矩形的左上角的 y 坐标。

  • rect.z 是矩形的宽度。

  • rect.w 是矩形的高度。

  • 矩阵

在GLSL中矩阵拥有2 * 2、3 * 3、4 * 4三种类型的矩阵,分别用mat2、mat3、mat4表示。
我们可以把矩阵看做是一个二维数组,也可以用二维数组下表的方式取里面具体位置的值。
现在我们已经讨论了向量的全部内容,是时候看看矩阵了!简单来说矩阵就是一个矩形的数字、符号或表达式数组。矩阵中每一项叫做矩阵的元素(Element)。下面是一个2×3矩阵的例子: image

矩阵可以通过(i, j)进行索引,i是行,j是列,这就是上面的矩阵叫做2×3矩阵的原因(3列2行,也叫做矩阵的维度(Dimension))。这与你在索引2D图像时的(x, y)相反,获取4的索引是(2, 1)(第二行,第一列)(译注:如果是图像索引应该是(1, 2),先算列,再算行)

矩阵的构造方法则更加灵活,有以下规则:

  • 如果对矩阵构造器只提供了一个标量参数,该值会作为矩阵的对角线上的值。例如 mat4(1.0) 可以构造一个 4 × 4 的单位矩阵
  • 矩阵可以通过多个向量作为参数来构造,例如一个 mat2 可以通过两个 vec2 来构造
  • 矩阵可以通过多个标量作为参数来构造,矩阵中每个值对应一个标量,按照从左到右的顺序
mat3 myMat3 = mat3(1.0, 0.0, 0.0,  // 第一列
                   0.0, 1.0, 0.0,  // 第二列
                   0.0, 1.0, 1.0); // 第三列

三维图形学中我们只用到4x4矩阵,它能对顶点(x,y,z,w)作变换。这一变换是用矩阵左乘顶点来实现的:

矩阵x顶点(记住顺序!!矩阵左乘顶点,顶点用列向量表示)= 变换后的顶点

image

这看上去复杂,实则不然。左手指着a,右手指着x,得到ax。 左手移向右边一个数b,右手移向下一个数y,得到by。依次类推,得到cz、dw。最后求和ax + by + cz + dw,就得到了新的x!每一行都这么算下去,就得到了新的(x, y, z, w)向量。

这种重复无聊的计算就让计算机代劳吧。

用C++,GLM表示:

glm::mat4 myMatrix;
glm::vec4 myVector;
// fill myMatrix and myVector somehow
glm::vec4 transformedVector = myMatrix * myVector; // Again, in this order ! this is important.

用GLSL表示:

mat4 myMatrix;
vec4 myVector;
// fill myMatrix and myVector somehow
vec4 transformedVector = myMatrix * myVector; // Yeah, it's pretty much the same than GLM
  • 采样器

    采样器是专门用来对纹理进行采样工作的,在GLSL中一般来说,一个采样器变量表示一副或者一套纹理贴图。所谓的纹理贴图可以理解为我们看到的物体上的皮肤。

  • 结构体

    和C语言中的结构体相同,用struct来定义结构体。

  • 数组

    数组也与C相同

  • 空类型

    void

变量修饰符

  • none:(默认的可省略)本地变量,可读可写,函数的输入参数既是这种类型
  • const:常量
  • in:输入变量,一般用于各个顶点各不相同的量。如顶点颜色、坐标等。用于保存顶点或法线数据,它可以在数据缓冲区中读取数据,仅能用于顶点着色器。(GLSL 100 es版本中是attribute)
  • uniform:统一变量。统一变量存储应用程序通过OpenGL ES API传入着色器的只读值。在运行时 shader 无法改变 uniform 变量,一般用来放置程序传递给 shader 的变换矩阵,材质,光照参数等等,可用于顶点着色器和片元着色器。如果统一变量在顶点着色器和片段着色器中均有声明,则声明的类型必须相同,且两个着色器中的值也需要相同。
  • out:输出变量,易变量,用于修饰从顶点着色器向片元着色器传递的变量。一般是在光栅化图元的过程中计算生成,记录在每个片段中(而不是从顶点着色器直接传递给片元着色器),(GLSL 100 es版本中是varying)

运算符

[]、++、-、+、?、:、> 等等

类型转换

GLSL的类型转换与C不同。在GLSL中类型不可以自动提升,比如float a=1;就是一种错误的写法,必须严格的写成float a=1.0,也不可以强制转换,即float a=(float)1;也是错误的写法,但是可以用内置函数来进行转换,如float a=float(1);还有float a=float(true);(true为1.0,false为0.0)等,值得注意的是,低精度的int不能转换为低精度的float

流程控制

常用的与java基本一样

函数

定义函数的方式也与C语言基本相同。函数的返回值可以是GLSL中的除了采样器的任意类型。对于GLSL中函数的参数,可以用参数用途修饰符来进行修饰,常用修饰符如下:

  • in:输入参数,无修饰符时默认为此修饰符。

  • out:输出参数。

  • inout:既可以作为输入参数,又可以作为输出参数。

  • 浮点精度

    与顶点着色器不同的是,在片元着色器中使用浮点型时,必须指定浮点类型的精度,否则编译会报错。精度有三种,分别为:

    • lowp:低精度。8位。
    • mediump:中精度。10位。
    • highp:高精度。16位。
  • 程序结构

    也是main()为入口函数、全局变量、局部变量等,与java类似。

GLSL内建变量

在着色器中有一些特殊的变量,不用声明也可以使用,这些变量叫做内建变量。 他们大致可以分为两种,一种是input类型,负责向硬件(渲染管线)发送数据;另一种是output类型,负责向程序回传数据,以便编程时需要。内建变量相当于着色器硬件的输入和输出点,使用者利用这些输入点输入之后,就会看到屏幕上的输出。通过输出点可以知道输出的某些数据内容。

顶点着色器的内建变量

  • 输入变量
    • gl_Position:顶点坐标信息
    • gl_PointSize:点的大小,默认是1,只有在gl.POINTS模式下才有效

片段着色器的内建变量

  • 输入变量
    • gl_FragCoord:当前片元在framebuffer画面的相对位置(输入片段的坐标)
    • gl_FragFacing:bool型,表示是否为属于光栅化生成此片元的对应图元的正面。
    • gl_PointCoord:经过插值计算后的纹理坐标,点的范围是0.0到1.0
  • 输出变量
    • gl_FragColor:当前片元颜色(GLSL 200 es版本的内置属性,在GLSL 300 es中已经没有了,需要自己用out关键字定义)
    • gl_FragData:vec4类型的数据。设置当前片元的颜色,供渲染管线的后继过程使用。(GLSL 200 es版本的内置属性,在GLSL 300 es中已经没有了,需要自己用out关键字定义)

内置函数

常用函数

  • radians(x):角度转弧度
  • degrees(x):弧度转角度
  • sin(x):正弦函数,传入值为弧度。相同的还有cos余弦函数、tan正切函数、asin反正弦、acos反余弦
  • atan():反正切
  • pow(x,y):xy
  • exp(x):ex
  • exp2(x):2x
  • log(x):logex
  • log2(x):log2x
  • sqrt(x):x√
  • inversesqr(x):1x√
  • abs(x):取x的绝对值
  • sign(x):x>0返回1.0,x<0返回-1.0,否则返回0.0
  • ceil(x):返回大于或者等于x的整数
  • floor(x):返回小于或者等于x的整数
  • fract(x):返回x-floor(x)的值
  • mod(x,y):取模(求余)
  • min(x,y):获取xy中小的那个
  • max(x,y):获取xy中大的那个
  • mix(x,y,a):返回x∗(1−a)+y∗a
  • step(x,a):x< a返回0.0,否则返回1.0
  • smoothstep(x,y,a):a < x返回0.0,a>y返回1.0,否则返回0.0-1.0之间平滑的Hermite插值。
  • dFdx(p):p在x方向上的偏导数
  • dFdy(p):p在y方向上的偏导数
  • fwidth(p):p在x和y方向上的偏导数的绝对值之和

几何函数

  • length(x):计算向量x的长度
  • distance(x,y):返回向量xy之间的距离
  • dot(x,y):返回向量xy的点积
  • cross(x,y):返回向量xy的差积
  • normalize(x):返回与x向量方向相同,长度为1的向量

矩阵函数

  • matrixCompMult(x,y):将矩阵相乘
  • lessThan(x,y):返回向量xy的各个分量执行x< y的结果,类似的有greaterThan,equal,notEqual
  • lessThanEqual(x,y):返回向量xy的各个分量执行x<= y的结果,类似的有类似的有greaterThanEqual
  • any(bvec x):x有一个元素为true,则为true
  • all(bvec x):x所有元素为true,则返回true,否则返回false
  • not(bvec x):x所有分量执行逻辑非运算

更多

在前面的教程中,我们了解了如何填充VBO、配置顶点属性指针以及如何把它们都储存到一个VAO里。
这次,我们同样打算把颜色数据加进顶点数据中。我们将把颜色数据添加为3个float值至vertices数组。我们将把三角形的三个角分别指定为红色、绿色和蓝色:

float vertices[] = {
    // 位置              // 颜色
     0.5f, -0.5f, 0.0f,  1.0f, 0.0f, 0.0f,   // 右下
    -0.5f, -0.5f, 0.0f,  0.0f, 1.0f, 0.0f,   // 左下
     0.0f,  0.5f, 0.0f,  0.0f, 0.0f, 1.0f    // 顶部
};

由于现在有更多的数据要发送到顶点着色器,我们有必要去调整一下顶点着色器,使它能够接收颜色值作为一个顶点属性输入。
需要注意的是我们用layout标识符来把aColor属性的位置值设置为1:

#version 330 core
layout (location = 0) in vec3 aPos;   // 位置变量的属性位置值为 0 
layout (location = 1) in vec3 aColor; // 颜色变量的属性位置值为 1

out vec3 ourColor; // 向片段着色器输出一个颜色

void main()
{
    gl_Position = vec4(aPos, 1.0);
    ourColor = aColor; // 将ourColor设置为我们从顶点数据那里得到的输入颜色
}

由于我们不再使用uniform来传递片段的颜色了,现在使用ourColor输出变量,我们必须再修改一下片段着色器:

#version 330 core
out vec4 FragColor;  
in vec3 ourColor;

void main()
{
    FragColor = vec4(ourColor, 1.0);
}

因为我们添加了另一个顶点属性,并且更新了VBO的内存,我们就必须重新配置顶点属性指针。更新后的VBO内存中的数据现在看起来像这样:

image

知道了现在使用的布局,我们就可以使用glVertexAttribPointer函数更新顶点格式:

// 位置属性
GLES30.glVertexAttribPointer(0, 3, GLES30.GL_FLOAT, false, 6 * Float.BYTES, 0);
GLES30.glEnableVertexAttribArray(0);
// 颜色属性
GLES30.glVertexAttribPointer(1, 3, GLES30.GL_FLOAT, false, 6 * Float.BYTES, 3* Float.BYTES);
// 1是对应上面着色器代码中的位置: layout (location = 1) in vec3 aColor; 颜色变量的属性位置值为 1
GLES30.glEnableVertexAttribArray(1);

glVertexAttribPointer函数的前几个参数比较明了。这次我们配置属性位置值为1的顶点属性。颜色值有3个float那么大,我们不去标准化这些值。
由于我们现在有了两个顶点属性,我们不得不重新计算步长值。为获得数据队列中下一个属性值(比如位置向量的下个x分量)我们必须向右移动6个float,其中3个是位置值,另外3个是颜色值。
这使我们的步长值为6乘以float的字节数(=24字节)。
同样,这次我们必须指定一个偏移量。对于每个顶点来说,位置顶点属性在前,所以它的偏移量是0。颜色属性紧随位置数据之后,所以偏移量就是Float.BYTES,用字节来计算就是12字节。

着色器示例

顶点着色器如下:
#version 430

layout (location=0) in vec3 position;
uniform mat4 mv_matrix;
uniform mat4 proj_matrix;

out vec4 varyingColor;

void main(void)
{  gl_Position = proj_matrix * mv_matrix * vec4(position,1.0);
    varyingColor = vec4(position,1.0) * 0.5 + vec4(0.5, 0.5, 0.5, 0.5);
}


着色器如下:
#version 430

in vec4 varyingColor;

out vec4 color;
uniform mat4 mv_matrix;
uniform mat4 proj_matrix;

void main(void)
{  color = varyingColor;
}
请注意,因为颜色是从顶点着色器的顶点属性varyingColor中发出的,所以它们也由光栅着色器进行插值!

请注意,代码中将位置坐标乘1/2,然后加1/2,以将取值区间从[−1, +1]转换为[0, 1]。此外,由程序员定义的插值顶点属性变量名称中通常包含单词“varying”,这是一种约定俗成的做法。修改的具体位置已突出显示。