OpenGL 着色语言
基本类型:
类型 | 说明 |
---|---|
int | 有符号二进制补码的32位整数 |
uint | 无符号的32位整数 |
float | IEEE 32位浮点值 |
double | IEEE 64位浮点值 |
void | 空类型 |
bool | 布尔类型 true,false |
GLSL隐式类型转换
所需的类型 | 可以从这些类型隐式转换 |
---|---|
uint | int |
float | int、uint |
double | int、uint、float |
GLSL 隐式类型转换来自于 《OpenGL 编程指南 第八版》;而在 《OpenGL ES 3.0 编程指南》 中这样写道:
OpenGL ES 着色语言在类型转换方面有着非常严格的规则;也就是说,变量只能赋值为相同类型的其他变量或者与相同类型的变量进行运算。在语言中不允许隐含类型的转换的原因是:可以避免着色器作者遇到可能导致难以跟踪的缺陷的意外转换;以下隐式类型的转换在OpenGL ES 中会报错:
float myFloat = 1; //error:invalid type conversion
聚合类型
1. 对角矩阵
2. 矩阵的构建
矩阵的构建需要遵守列主序的原则,传入的数据将首先填充列,然后填充行(如下矩阵的不同表示形式)。
3. 访问向量和矩阵中的元素
分量访问符 | 符号描述 |
---|---|
(x,y,z,w) | 与位置相关的分量 |
(r,g,b,a) | 与颜色相关的分量 |
(s,t,p,q) | 与纹理坐标相关的分量 |
这种分量的访问符的一种常用应用叫做 swizzle ,对于颜色的处理,比如颜色空间的转换时可能会用到它,如基于颜色的红色分量来设置一个亮度值:
vec3 luminance = color.rrr;
//v.xyzw 其中xyzw 可以任意组合
//v.rgba 其中rgba 可以任意组合
//v.stpq 其中stpq 可以任意组合
//唯一的限制是:在一条语句的一个变量中,只能使用一种类型访问符
vec4 v=vec4(1.0,2.0,3.0,1.0);
float x = v.x; //1.0
float x1 = v.r; //1.0
float x2 = v[0]; //1.0
vec3 xyz = v.xyz; //vec3(1.0,2.0,3.0)
vec3 xyz1 = vec(v[0],v[1],v[2]); //vec3(1.0,2.0,3.0)
vec3 rgb = v.rgb; //vec3(1.0,2.0,3.0)
结构体:
如果定义了一个结构体,那么它会自动创建一个新的类型,并且隐式定义一个构造函数,将各个类型的结构体参数作为输入参数。
struct Particle{
float lifetime;
vec3 position;
vec3 velocity;
}
Particle p = Particle(10.0,pos,val);
float time = p.lifetime;
vec3 pos = p.position;
数组:
数组可以定义为有大小的,或者没有大小的;可以使用没有大小的数组作为一个前置申明,然后重新用一个合适的大小来声明它。
float coeff[3];
float[3] coeff;
int indices[];
float coeff[3] = float[3](2.0,3.0,3.3);
GLSL 的数组 与JAVA 类似,他有一个隐式的方法可以返回元素的个数:length();
向量和矩阵类型也可以使用length() 方法:
向量的长度也就是它包含的元素个数;
矩阵的长度是它包含列的个数
变量限定符:
修饰符 | 说明 |
---|---|
const | 将一个变量设置为只读形式,如果它初始化时用的是一个编译器常量,那么它本身也会成为编译器常量 |
in | 设置这个变量为着色器的输入变量 |
out | 设置这个变量为着色器的输出变量 |
uniform | 设置这个变量为用户应用程序传递给着色器的数据,它对于给定的图元是一个常量 |
buffer | 设置应用程序共享的一块可读写的内存;这块内存也作为着色器的存储缓存使用 |
shared | 设置变量是本地工作组中共享的,它只能用于计算着色器中 |
attribute变量、varying变量
attribute变量是只能在vertex shader中使用的变量。(它不能在fragment shader中声明attribute变量,也不能被fragment shader中使用);
varying变量主要用于在Shader Stage间进行传递,注意的是在光栅化(Rasterization)的时候,这些变量也会跟着一起被光栅插值。同样在GL3.x 中 varying 关键字也被废弃。
在GL3.x中,废弃了attribute、varying关键字,属性变量统一用in/out作为前置关键字,对每一个Shader stage来说,in表示该属性是作为输入的属性,out表示该属性是用于输出的属性。
const:
和C语言类似,被const限定符修饰的变量初始化后不可变,除了局部变量,函数参数也可以使用const修饰符;
const变量必须在声明时就初始化 const vec3 v3 = vec3(0.,0.,0.)
。
struct light {
vec4 color;
vec3 pos;
//const vec3 pos1; //结构中的字段不可用const修饰会报错.
};
const light lgt = light(vec4(1.0), vec3(0.0)); //结构变量可以用const修饰
in:
in 修饰符用于定义着色器阶段的输入变量,这类输入可以是顶点属性(顶点着色器),或者是前一个着色器阶段的输出变量。
in type in_variable_name;
out:
out修饰符用于定义一个着色器阶段的输出变量,例如:顶点着色器中输出变换后的齐次坐标,或者像素着色器中输出的最终片元颜色。
//顶点着色器
#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;
}
uniform:
uniform变量是全局
且只读
的,在整个shader执行完毕前其值不会改变,他可以和任意基本类型变量组合,一般我们使用uniform变量来放置外部程序传递来的环境数据(如点光源位置,模型的变换矩阵等等),这些数据在运行中显然是不需要被改变的。
uniform vec4 BaseColor;
buffer:
如果需要在应用程序中共享一大块缓存给着色器,那么最好的方法是使用buffer变量;buffer 修饰符指定随后的块作为着色器与应用程序共享的一块内存缓存;这块缓存对着色器来说是可读可写的,缓存的大小可以在着色器编译和程序链接完成后设置。
shared:
shared修饰符只能用于计算着色器中,它可以建立本地工作组内共享的内存。
运算符:
操作符重载
GLSL 中大部分的操作符都是经过重载的,也就说他们可用于多种类型的数据操作;特别是矩阵和向量的算术操作符(包括前置和后置的“++”和“--”),在GLSL 中都是进过严格定义的。
1. 向量和矩阵的乘法
基本的限制条件:矩阵和向量的维度必须是匹配的
vec3 v;
mat3 m;
vec3 result = v * m;
2. 向量与向量的乘法
vec2 a, b, c;
c = a* b ;// c = (a.x * b.x , a.y * b.y)
函数参数限定符:
函数的参数默认是以拷贝的形式传递的,也就是值传递,任何传递给函数参数的变量,其值都会被复制一份,然后再交给函数内部进行处理.我们可以为参数添加限定符来达到传递引用的目的,glsl中提供的参数限定符如下:
流控制
glsl的流控制和c语言非常相似,这里不必再做过多说明,唯一不同的是片段着色器中有一种特殊的控制流discard;
使用discard会退出片段着色器,不执行后面的片段着色操作。片段也不会写入帧缓冲区。
for (l = 0; l < numLights; l++)
{
if (!lightExists[l]);
continue;
color += light[l];
}
...
while (i < num)
{
sum += color[i];
i++;
}
...
do{
color += light[lightNum];
lightNum--;
}while (lightNum > 0)
...
if (true)
discard;
glsl的函数:
glsl允许在程序的最外部声明函数.函数不能嵌套,不能递归调用,且必须声明返回值类型(无返回值时声明为void) 在其他方面glsl函数与c函数非常类似.
vec4 getPosition(){
vec4 v4 = vec4(0.,0.,0.,1.);
return v4;
}
void doubleSize(inout float size){
size= size*2.0 ;
}
void main() {
float psize= 10.0;
doubleSize(psize);
gl_Position = getPosition();
gl_PointSize = psize;
}
构造函数:
glsl中变量可以在声明的时候初始化,float pSize = 10.0
也可以先声明然后等需要的时候在进行赋值;聚合类型对象如(向量,矩阵,数组,结构) 需要使用其构造函数来进行初始化. vec4 color = vec4(0.0, 1.0, 0.0, 1.0);
//一般类型
float pSize = 10.0;
float pSize1;
pSize1=10.0;
...
//复合类型
vec4 color = vec4(0.0, 1.0, 0.0, 1.0);
vec4 color1;
color1 =vec4(0.0, 1.0, 0.0, 1.0);
...
//结构
struct light {
float intensity;
vec3 position;
};
light lightVar = light(3.0, vec3(1.0, 2.0, 3.0));
//数组
const float c[3] = float[3](5.0, 7.2, 1.1);
类型转换:
glsl可以使用构造函数进行显式类型转换,各值如下:
bool t= true;
bool f = false;
int a = int(t); //true转换为1或1.0
int a1 = int(f);//false转换为0或0.0
float b = float(t);
float b1 = float(f);
bool c = bool(0);//0或0.0转换为false
bool c1 = bool(1);//非0转换为true
bool d = bool(0.0);
bool d1 = bool(1.0);
计算不变性
GLSL无法保证在不同的着色器中,两个完全相同的计算式得到完全一样的结果;不同的优化方式可能导致结果非常细微的差异,这些细微的差异对于多通道的算法会差生问题,因为各个着色器阶段可能需要计算得到完全一致的结果;GLSL 有以下两种方法来确保着色器之间的计算不变性:invariant 和 precise。
这两种方法都需要在图形设备上完成计算过程,来确保同一个表达式的结果可以保证不变性;但是对于宿主计算机和图形硬件各自的计算,这两种方法都无法保证结果的完全一致性。
invariant:
invariant 限定符可以设置任何着色器的输出变量;他可以确保如果两个着色器的输出变量使用了同样的表达式,并且表达式中的变量也是相同的值,那么计算的结果也是相同的。
invariant gl_Position;//可以将一个内置的输出变量声明为invariant
invariant centroid out vec3 Color; //也可以声明一个自定义的变量
在调试过程中,可以通过顶点着色器的预编译命令pragma 来完成将所有的输出变量全部设置为invariant;全局设置可以帮助我们解决调试问题,但是:这样对于着色器的性能也会有所影响,为了不变性,通常会导致GLSL编译器所执行的优化工作被停止。
#pragma STDGL invariant(all)
precise:
precise限制符可以设置任何计算中的变量或者函数的返回值;在着色器中,关键字 precise 可以在使用某个变量之前的任何位置上设置这个变量,并且可以修改之前已经声明过的变量。
如果必须保证某一个表达式产生的结果是一致的,即使表达式的数据发生了变化(但是在数学上并不影响结果)也是如此,那么此时应该使用precise而非invariant,
/*
* a,b 交换
* c,d 交换
* a b 和 c d 交换
* 以上三种情况都应该得到同样的结果
*/
Location = a * b + c * d;
预编译指令:
内置的宏:
//宏定义
#define NUM_ELEMENT 10
#define LPos(n) gl_LightSource[(n)].position
//取消宏定义
#undef LPos
//宏的条件分支
#ifdef NUM_ELEMENT
...
#endif
#if ...
...
#elif ...
...
#endif
编译器的控制
#pragma 命令可以向编译器传递附加信息,并在着色器代码编译时设置一些额外的属性。
//开启、禁用着色器优化
//必须在函数定义的代码块之外设置,一般默认开启优化
#pragma optimize(on)
#pragma optimize(off)
//编译器调试选项
#pragma debug(on)
#pragma debug(off)
着色器的编译
对于每个着色器程序,都需要在应用程序中通过如下的步骤进行设置:
- 创建一个着色器对象
- 将着色器源码编译为对象
- 验证着色器的编译是否成功
unsigned int vertex, fragment;
int success;
char infoLog[512];
// 顶点着色器
vertex = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertex, 1, &vShaderCode, NULL);
glCompileShader(vertex);
// 打印编译错误(如果有的话)
glGetShaderiv(vertex, GL_COMPILE_STATUS, &success);
if(!success)
{
glGetShaderInfoLog(vertex, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
};
然后将多个着色器对象链接为一个着色器程序:
- 创建一个着色器程序
- 将着色器对象关联到着色器程序
- 链接着色器程序
- 判断着色器的链接过程是否成功完成
- 使用着色器来处理顶点和片元
// 着色器程序
ID = glCreateProgram();
glAttachShader(ID, vertex);
glAttachShader(ID, fragment);
glLinkProgram(ID);
// 打印连接错误(如果有的话)
glGetProgramiv(ID, GL_LINK_STATUS, &success);
if(!success)
{
glGetProgramInfoLog(ID, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
}
以上参考:
- OpenGL 编程指南 (原书第八版)
- OpenGL 着色语言(Randi J.Rost 著)
- OpenGL ES 3.0 编程指南
- https://github.com/wshxbqq/GLSL-Card