OpenGL ES _ 着色器_语法
OpenGL ES _ 入门_01
OpenGL ES _ 入门_02
OpenGL ES _ 入门_03
OpenGL ES _ 入门_04
OpenGL ES _ 入门_05
OpenGL ES _ 入门练习_01
OpenGL ES _ 入门练习_02
OpenGL ES _ 入门练习_03
OpenGL ES _ 入门练习_04
OpenGL ES _ 入门练习_05
OpenGL ES _ 入门练习_06
OpenGL ES _ 着色器 _ 介绍
OpenGL ES _ 着色器 _ 程序
OpenGL ES _ 着色器 _ 语法
OpenGL ES_着色器_纹理图像
OpenGL ES_着色器_预处理
OpenGL ES_着色器_顶点着色器详解
OpenGL ES_着色器_片断着色器详解
OpenGL ES_着色器_实战01
OpenGL ES_着色器_实战02
OpenGL ES_着色器_实战03
学习那些内容
- 程序从什么地方执行
- 声明变量
- 构造函数
- 聚合类型
- 如何访问向量和矩阵中的元素
- 结构
- 数组
- 类型限定符
- uniform 块
- 语句
- 函数
你不知道我在说什么,请从这里开始,以上就是我们今天要讲的内容,(OpenGL Shading Language)加油!
内容详细讲解
-
程序的起点
着色器程序就像C 程序一样,是从main() 函数开始的,每个GLSL 着色器函数都是从下面结构开始执行的void main(){ //code }
注释也是使用// 或者“/”和"/"
- 变量
首先要说一点,GLES 是一种强类型的语言,强类型形语言有个特点,每个变量必须进行声明,Swift 也是强类型语言,那为什么不用声明变量呢,因为它可以进行类型推断。GLES 有自己的变量类型,变量命名与c语言一样,可以使用字母,_ 和数字,但变量名的第一个字符不能是数字。
|类型 |描述 |
| ---------------|
|Float | 浮点类型 |
|int |有符号整型 |
|uint | 无符号整型|
|bool |布尔型 |
变量的作用域,和c语言一样,举个例子
for(int i=0,i<10;++i){
// loop body
}
i 的作用域仅限于循环体内
变量的初始化
整型变量可以使用八进制,十进制,十六进制表示
浮点数必须包含一个小数点,并且可以向c语言中一样后面加个F或者f,也可以使用科学计数法表示
布尔值为true或者face
int i,num =1500;
float time = 1.23f;
bool isRead = false;
不同类型的值不能进行隐式转换,比如int i = 10.3 编译器会报错的,那如何处理,我们需要借助构造函数 比如 :
float f = 10.1;
int t = int(f);
- 聚合类型
上面已经把基本类型讲过了,GLSL 基本类型可以进行组合使用,这样做的好处是能够和OpenGL 的数据相匹配,简化计算方法,GLSL 支持每种类基本型的二维,三维,四维的矢量运算,以及浮点类型的22,33,4*4 的浮点矩阵.
|基本类型|二维向量|三维向量|四维向量|矩阵类型|
|-----|
|float|vec2|vec3|vec4|mat2,mat3,mat4<p>mat2x2,mat2x3,mat4x4,</p><p>mat3x2,mat3x3,mat3x4,</p><p>mat4x2,mat4x3,mat4x4</p>|
|int|ivec2|ivec3|ivec4|...|
|uint|uvec2|uvec3|uvec4|...|
|bool|bvec2|bvec3|bvec4|...|
怎么初始化
vec3 g = vec3(0.0,-9.8,3.0)
类型转换
ivec3 ig = ivec3(g)
使用向量构造函数,将向量进行截短
vec4 color;
vec3 RGB = vec3(color);
使用构造函数,将向量进行拉长
vec3 RGB;
vec4 RGBA = vec4(RGB,0.5);
矩阵的构建
初始化为对角矩阵
mat3 m = mat3(1.0)
初始化为完整矩阵
mat3 m = mat3(1.0,2.0,3.0,
4.0,5.0,6.0,
7.0,8.0,9.0,)
还可以这样初始化
vec3 v1 = vec3(1.0,2.0,3.0)
vec3 v2 = vec3(1.0,2.0,3.0)
vec3 v3 = vec3(1.0,2.0,3.0)
mat3 m = mat3(v1,v2,v3)
你以为结束了吗,还可以这样初始化
vec2 col1 = vec2(1.0,2.0)
vec2 col2 = vec2(1.0,2.0)
vec2 col3 = vec2(1.0,2.0)
mat3 m = mat3(col1,1.0
col2,2.0,
col3,3.0)
接下来,讲一下如何访问向量和矩阵中的元素,大学中学过的,可能大家有些遗忘,那就带大家回顾一下.
访问向量
//可以通过名称访问向量
float red = color.r;
float v_y = velocity.y;
// 可以通过下标访问
float red = color[0];
float v_y = velocity[1];
//向量的另外一种访问方式,叫做搅拌式成分访问
vec3 lum = color.rrr;
/// 移动向量的成分
vec4 color = color.abgr;
/// 唯一的限制是,一组向量只能使用一组成分,下面这样是错误的
vec4 color = color.rgza;
/// 如果访问超过范围也会报错
vec2 pos;
float z = pos.z;
访问矩阵
mat4 m = mat4(3.0);
vec4 zvec = mat4[2];
float yScale = m[1][1];
|成分访问名称|描述|
|---|
|(x,y,z,w)|位置相关|
|(r,g,b,a)|颜色相关|
|(s,t,p,q)|纹理坐标相关|
结构体
为甚要用结构体,结构体能将不同类型的数据从逻辑上结合在一起,结构体可以方便的把一组相关的数据传递给函数
struct Sun{
float r;
vec3 position;
vec3 velour;
}
数组
GLSL 还支持数组类型,和c语言一样,很简单,写个例子大家看一下
// 声明
float off[3];
float[3] coffe;
int indices[];
// 初始化
float coif[3] = float[3](1.0,1.0,1.0);
// GLES 数组提供了一个隐士的方法length() 获取数组长度
int length = coif.length()
类型限定符
顶点着色器的输入变量用关键字attribute 来限定
片段着色器的输入变量用关键字varying 来限定
注意在GLSL 1.4 中attribute 和varying都被删除,使用通用的 in,out 表示输入和输出
请看表
|类型限定符|描述|
|---|
|const|把变量标记为只读的编译器常量|
|in|指定变量量为着色器阶段的一个输入|
|out|指定变量为着色器的阶段的一个输出|
|uniform|指定这个值应从应用程序传给着色器,并在一个特定的图元中保持常量|
重点讲解一下关键字in的使用
in 用来限定着色器的输入,可能是顶点着色器或者片段着色器,片段着色器可以近一步进行限定
|in关键字限定符|说明|
|---|
|centroid|打开多采样,强制一个片段输入变量采样位于图元像素覆盖区域|
|smooth|以透视校正的方式插值片段输入变量|
|flat|不对片段输入差值|
|noperspective|线性差值片段变量|
out 类型限定符
用来限定着色器阶段的输出,顶点着色器可以使用centroid关键字限定输出,该关键字在片段着色器中也必须使用centroid 来限定一个输入(也就是说片段着色器中必须有一个和顶点着色器相同声明的变量)
uniform 类型限定符
uniform 限定了表示一个变量的值将有应用程序在着色器执行之前指定,并且在图元处理过程中不会发生变化,uniform 变量是有顶点着色器和片段着色器共享的,他们必须声明为全局变量
怎么使用呢?思考这样一个问题:创建一个着色器给图元使用这个指定的颜色着色.可以这样声明
uniform vec4 BaseColor;
思考: 在着色器内部可以通过名字来引用它,但是在程序中,我们应该如何设置它的值呢?
答:当GLSL 编译器连接到着色器程序中后,他会创建一个表格,其中包含了所有uniform 变量。为了在应用程序中设置BaseColor 的值,需要获取BaseColor 在表中的连接。这个是通过下面的函数获取的.
Glint glGetUniformLocation(GLuint program,const char *name)
参数 1:program 程序的标识
参数2 :name 着色器变量值得名称 如:"BaseColor" ,对于变量是数组的情况,可以直接指定数组名(array),也可以指定第一个元素的索引(array[0])
问:现在我们已经获取到了这个变量的值了,那怎么使用设置它的值呢?
答:可以使用下面的函数去设置它的值:
void glUniform*()
void glUniformMatrix*()
上面不是两个函数,是两类函数如 glUniform1f()
time = glGetUniformLocation(program,"time ");// 获取
glUniform(timeLoc,timeValue);// 设定值
uniform 块
问:为什么要引用uniform 块,它能解决什么问题?
答:大家有没有想过,当着色器程序复杂的时候,我们如何管理不同着色器程序和uniform 变量之间的关系,在连接着色器的时候,调用glLink的时候,产生uniform 位置,索引可能会发生变化,即便uniform变量的值是相同的,统一缓冲区对象提供了一种方法,既优化uniform变量的访问,又可以使用跨着着色器共享uniform值.
先看一段代码
uniform Matrices {
mat4 ModelView
mat4 ProjectView
mat4 color
}
这个就是uniform 块的声明,这个uniform 变量集合可以使用glMapBuffer() 这样的程序进行访问.
除了采样器,所有的类型,都允许放在一个uniform 块中,注意 ,uniform 块必须声明为全局域.
uniform 块布局
|布局限定符|说明|
|---|
|shared|指定uniform块在多个程序之间共享|
|packed|布局uniform块以使其使用的内存最小化,然而,这通常不允许块程序共享|
|std140|为uniform块使用OpenGL 规范描述默认布局|
|row_major|使的uniform快中的矩阵按照行主序的方式存储|
|column_major|指定矩阵应该按照主序的方式存储|
怎么使用,看下面代码
layout(shared,row_major) uniform{...} // 指定单一的uniform 块
layout(packed,column_major) uniform;// 括号中的多个限定选项必须用逗号隔开,要影响到所有后续uniform块的布局,这样指定所有uniform块都讲使用该布局,知道全局布局修改,或者自己包含一个布局,覆盖对全局的声明指定。
问: 怎么对uniform块进行访问呢?
第一步.获取uniform块索引
GLuint gLGetUniformBlockIndex(GLuint program,const char* uniformBlockName)
返回和program相关的uniformBlockName 所指定的具名uniform 的索引,如果uniformBlockName 不是一个有效的uniform块,则返回GL_INVALID_INDEX.
第二步. 初始化一个缓冲区
使用glBindBuffer() 把缓冲区对象绑定到一个GL_UNIFORM_BUFFER 目标
第三步 . 确定着色器这个uniform块需要多大的空间
使用glGetActiveUniformBlockiv()来请求GL_UNIFORM_BLOCK_DATA_SIZE ,它返回了编译器生成的块的大小。
第四步。绑定
void glBindBufferRange(GLenum target,GLunit index,GLuint buffer,GLintptr offset,GLsizeiptr size);
void glBindBufferBase(GLenum target,GLuint index,GLuint buffer)
上面两个函数的作用,是讲缓冲区对象buffer 和 index 相关的uniform块关联起来,
参数1: target 可以是GL_UNIFORM_BUFFER 或者GL_TRANSFORM_FEEDBACK_BUFFER(用于变换反馈)
参数2:index 是和uniform相关的索引
参数3: buffer 缓冲区标识
参数4: offset 起始索引
参数5: size 大小
使用glBindBufferBase() 等同于使用offset等于0和size等于缓冲区对象的大小来调用glBindBufferRange()
调用这些函数有可能出现哪些bug:
size 小与0
offset+size 大于缓冲区大小
offset 或者size不是4的倍数
index 小与0
如果一个uniform和缓冲区对象建立的关系,可以使用影响缓冲区值得任何命令来初始化或者修改该块中的值。
思考: 如果多个着色器要共享一个uniform块,如何实现?
可以把一个指定名称的uniform块绑定到一个缓冲区对象,它避免了为每个程序分配一个不同的块索引。如何实现这种方式呢?在使用glLinkProgram() 之前,调用 glUniformBlockBinding()
Glint gUniformBlockBinding(GLuint program,GLuint uniformBlockIndex,GLuint uniformBlockBinding)
参数1: program 程序标识
参数2:uniformBlockIndex 程序块的索引
参数3:共享缓冲区的标识
思考:uniform 变量在一个uniform块中的布局,是由指定的布局限定符来控制的,而这是在编译和连接uniform块的时候进行的,如果使用默认的布局指定,需要确定uniform块中的每个变量的offset和数据存储size。为了做到这一点,我们将下面两个函数:
第一步:获取一个特定的uniform块的标识
void glGetUniformIndices(GLuint program,GLsizei uniformCount,const char **uniformName,GLuint *uniformIndices)
第二步. 调用glGetActiveUniformsiv()获取这个特定索引的offset和size
注意点
GLSL 并不能保证不同的着色器使用相同的计算产生相同的效果,这是因为,指令顺序累积的差别,编译后的指定顺序可能会差生微小的差别。
问题来了: 如果想要在每道着色器渲染时计算的位置完全相同,不然其出现这种微小的错误,怎么办呢?
答 :送你一个关键字 invariant ,强制不变型
invariant gl_position;
invariant centroid varying vec3 Color;
caring这个关键字,之前讲过,用于把顶点着色器的数据传给片段着色器,不变性变量,必须在顶点和片段着色器中都声明为invariant 。注意,可以在着色器中使用变量之前的任何使用对他应用的invariant关键字,并可以用他修改以前的变量。
小技巧:
在调试的时候,使用 #program STDGL inveriant(all) 就可以对所有varing 变量 加上不变形限制。可能性能会受点影响.因为保证不变性通常会进制GLSL 编译器所执行的那些优化。
语句
着色器真正工作是通过对值进行计算以及做出决策来完成的。CLSL 提供了一组简单操作符,便于创建更重算数操作来计算各种值。废话不多少,直接上表
|GLSL的操作符以及它们的优先级||||
|---|
|1|()|-|对操作进行聚组|
|2|[]|数组|数组下标|
|3|f()|函数|函数调用和构造器|
|4|.|结构|结构字段或方法访问|
|5|++ --|int、float、vec、mat*|后缀的自增或自减|
|6|++ --|int、float、vec、mat*|前缀的自增或自减|
|7|+ - !|int、float、vec、mat*|正、负、求反|
|8| /|int、float、vec、mat*|乘除操作|
|9|+ -|int、float、vec、mat*|加减操作|
|10|<> <= >=|int、float、vec、mat*|关系操作|
|11|== !=|int、float、vec、mat*|相等测试做操|
|12|&&|bool|逻辑与操作|
|13|^^|bool|逻辑异或操作|
|14|II|bool|逻辑或操作|
|15|a?b:c|bool、int、float、vec*、mat*、int、float、vec*、mat*|条件操作|
|16|=|int、float、vec*,mat*|赋值|
|17|+= -= *=/=|int、float、vec*,mat*|算数赋值|
|18|,|-|操作序列|
逻辑操作\循环结构 和 c语言一样,在这里就不过多说明.
- 流控制语句
|语句|描述|
|---|
|break| 终止循环块的执行,并接着执行循环块后的代码|
|continue|终止当前那次循环,然后继续执行下一次循环|
|return|从当前自程序返回,可以同时返回一个值|
|discard|丢弃当前的片段并且终止着色器执行。discard只能用在片段着色器|
函数
函数允许使用一个函数调用代替一段经常出现的代码
float HornerEvalPolynormial(float coiff[10],float x);
函数和C 语言几乎一样,唯一的不同就是变量访问的限定符,接下来你可能会问有哪些限定符不一样,请看下面的这张表
|访问限定符|描述|
|in|值赋值到函数中|
|const in|只读的值|
|out|从函数中复制出来的值(在传递给函数前未初始化)|
|inout|值赋值到函数中,并从函数中赋值出来|
总结
着色器基本的语法,已经说得查不多了。接下来,我们要开始进阶了,请大家持续关注!