GLSL语法基础简介#GLSL(OpenGL Shading Language) 全称 OpenGL 着色语言,是用来在 OpenGL 中着色编程的语言,也即开发人员写的短小的自定义程序,他们是在图形卡的 GPU上执行的,代替了固定的渲染管线的一部分,使渲染管线中不同层次具有可编程性。 GLSL 其使用 C 语言作为基础高阶着色语言,避免了使用汇编语言或硬件规格语言的复杂性。基础语法#注释#单行注释:// 多行注释:/* */变量命名GLSL的变量命名方式与C语言类似。变量的名称可以使用字母,数字以及下划线,但变量名不能以数字开头,还有变量名不能以gl_作为前缀,这个是GLSL保留的前缀,用于GLSL的内部变量。当然还有一些GLSL保留的名称是不能够作为变量的名称的。变量命名#GLSL 的变量命名方式与 C 语言类似,可使用字母,数字以及下划线,不能以数字开头。还需要注意的是,变量名不能以 gl_ 作为前缀,这个是 GLSL 保留的前缀,用于 GLSL 的内部变量。表达式#运算符优先级(越小越高) 运算符 说明 结合性1 () 聚组:a*(b+c) N/A2 [] () . ++ -- 数组下标__[],方法参数__fun(arg1,arg2,arg3),属性访问a.b,自增/减后缀a++ a-- L - R3 ++ -- + - ! 自增/减前缀++a --a,正负号(一般正号不写)a ,-a,取反!false R - L4 * / 乘除数学运算 L - R5 + - 加减数学运算 L - R7 < > <= >= 关系运算符 L - R8 == != 相等性运算符 L - R12 && 逻辑与 L - R13 ^^ 逻辑排他或(用处基本等于!=) L - R14 II 逻辑或 L - R15 ? : 三目运算符 L - R16 = += -= *= /= 赋值与复合赋值 L - R17 , 顺序分配运算 L - R左值与右值:左值:表示一个储存位置,可以是变量,也可以是表达式,但表达式最后的结果必须是一个储存位置.右值:表示一个值, 可以是一个变量或者表达式再或者纯粹的值.操作符的优先级:决定含有多个操作符的表达式的求值顺序,每个操作的优先级不同.操作符的结合性:决定相同优先级的操作符是从左到右计算,还是从右到左计算。操作符GLSL语言的操作符与C语言相似。如下表(操作符的优先级从高到低排列)操作符 描述() 用于表达式组合,函数调用,构造[] 数组下标,向量或矩阵的选择器. 结构体和向量的成员选择++ -- 前缀或后缀的自增自减操作符+ – ! 一元操作符,表示正 负 逻辑非* / 乘 除操作符+ - 二元操作符 表示加 减操作<> <= >= == != 小于,大于,小于等于, 大于等于,等于,不等于 判断符&& || ^^ 逻辑与 ,或, 异或?: 条件判断符= += –= *= /= 赋值操作符, 表示序列求地址的& 和 解引用的 * 操作符不再GLSL中出现,因为GLSL不能直接操作地址。类型转换操作也是不允许的。位操作符(&,|,^,~, <<, >> ,&=, |=, ^=, <<=, >>=)是GLSL保留的操作符,将来可能会被使用。求模操作(%,%=)也是保留的。数组访问数组的下标从0开始。合理的范围是[0, size - 1]。跟C语言一样。如果数组访问越界了,那行为是未定义的。如果着色器的编译器在编译时知道数组访问越界了,就会提示编译失败。vec4 myColor, ambient, diffuse[6], specular[6];myColor = ambient + diffuse[4] + specular[4];语句#流控制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;discard片段着色器中有一种特殊的控制流成为discard。使用discard会退出片段着色器,不执行后面的片段着色操作。片段也不会写入帧缓冲区。if (ture) discard;分量访问分量访问符 符号描述(x,y,z,w) 与位置相关的分量(r,g,b,a) 与颜色相关的分量(s,t,p,q) 与纹理坐标相关的分量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预编译指令#以 # 开头的是预编译指令,常用的有:#define #undef #if #ifdef #ifndef #else#elif #endif #error #pragma #extension #version #line比如 #version 100 他的意思是规定当前shader使用 GLSL ES 1.00标准进行编译,如果使用这条预编译指令,则他必须出现在程序的最开始位置.内置的宏:__LINE__ : 当前源码中的行号.__VERSION__ : 一个整数,指示当前的glsl版本 比如 100 ps: 100 = v1.00GL_ES : 如果当前是在 OPGL ES 环境中运行则 GL_ES 被设置成1,一般用来检查当前环境是不是 OPENGL ES.GL_FRAGMENT_PRECISION_HIGH : 如果当前系统glsl的片元着色器支持高浮点精度,则设置为1.一般用于检查着色器精度.实例:1.如何通过判断系统环境,来选择合适的精度:#ifdef GL_ES //#ifdef GL_FRAGMENT_PRECISION_HIGHprecision highp float;#elseprecision mediump float;#endif#endif2.自定义宏:#define NUM 100#if NUM==100#endif数据类型#基础数据类型#类型 描述void 跟 C 语言的 void 类似,表示空类型。作为函数的返回类型,表示这个函数不返回值。bool 布尔类型,true 或 false,以及可以产生布尔型的表达式。int 有符号整型uint 无符号整形float 浮点型纹理采样类型#类型 描述sampler1D 用于内建的纹理函数中引用指定的 1D纹理的句柄。只可以作为一致变量或者函数参数使用sampler2D 二维纹理句柄sampler3D 三维纹理句柄samplerCube cube map 纹理句柄sampler1DShadow 一维深度纹理句柄sampler2DShadow 二维深度纹理句柄聚合类型(向量和矩阵类型)#向量类型类型 描述vec2,vec3,vec4 2分量、3分量和4分量浮点向量ivec2,ivec3,ivec4 2分量、3分量和4分量整数向量uvec2,uvec3,uvec4 2分量、3分量和4分量无符号整数向量bvec2,vbec3,bvec4 2分量、3分量和4分量布尔向量A、向量声明--4分量的float 类型向量vec4 V1;B、声明向量并对其进行构造vec4 V2 = vec4(1,2,3,4);C、向量运算vec4 v;vec4 vOldPos = vec4(1,2,3,4);vec4 vOffset = vec4(1,2,3,4);//注意:接下来假设所有参与运算的变量已定义并赋值。v = vOldPos + vOffset;v = vNewPos;v += vec4(10,10,10,10);v = vOldPos * vOffset;v *= 5;D、向量元素的获取(成分选择)向量中单独的成分可以通过 {x,y,z,w}, {r,g,b,a} 或者 {s,t,p,q} 的记法来表示。这些不同的记法用于 顶点,颜色,纹理坐标。在成分选择中,你不可以混合使用这些记法。其中 {s,t,p,q} 中的 p 替换了纹理的 r 坐标,因为与颜色 r 重复了。下面是用法举例: 例如有向量 v1 和 v2:vec3 v1 = {0.5, 0.35, 0.7};vec4 v2 = {0.1, 0.2, 0.3, 0.4};可以通过 {x,y,z,w}, {r,g,b,a} 或者 {s,t,p,q} 来取出向量中的元素值。 通过 x,y,z,w:v2.x = 3.0f;v2.xy = vec2(3.0f,4.0f);v2.xyz = vec3(3,0f,4,0f,5.0f);通过 r,g,b,a:v2.r = 3.0f;v2.rgba = vec4(1.0f,1.0f,1.0f,1.0f);通过 s,t,q,r:v2.stqr = vec2(1.0f, 0.0f, 0.0f, 1.0f);错误示例:float myQ = v1.q;// 出错,数组越界访问,q代表第四个元素float myRY = v1.ry; // 不合法,混合使用记法向量还支持一次性对所有分量操作v1.x = v2.x +5.0f; v1.y = v2.y +4.0f; v1.z = v2.z +3.0f;v1.xyz = v2.xyz + vec3(5.0f,4.0f,3.0f);(2)矩阵类型类型 描述mat2 或 mat2x2 2x2的浮点数矩阵类型mat3 或 mat3x3 3x3的浮点数矩阵类型mat4 或 mat4x4 4x4的浮点数矩阵类型mat2x3 2列3行的浮点矩阵(OpenGL的矩阵是列主顺序的)mat2x4 2列4行的浮点矩阵mat3x2 3列2行的浮点矩阵mat3x4 3列4行的浮点矩阵mat4x2 4列2行的浮点矩阵mat4x3 4列3行的浮点矩阵创建矩阵:mat4 m1,m2,m3;构造单元矩阵:mat4 m2 = mat4(1.0f,0.0f,0.0f,0.0f 0.0f,1.0f,0.0f,0.0f, 0.0f,0.0f,1.0f,0.0f, 0.0f,0.0f,0.0f,1.0f);或者mat4 m4 = mat4(1.0f);向量矩阵运算#1、不同类型 float 与 int 间的运算:**float 与 int 之间进行运算,需要进行一次显示转换,以下表达式都是正确的:int a = int(2.0);float a = float(2);int a = int(2.0)*2 + 1;float a = float(2)*6.0+2.3;复制代码2、float 与 vec(向量)、mat(矩阵) 的运算:逐分量运算 vec,mat 这些类型其实是由 float 复合而成的,当它们与float 运算时,其实就是在每一个分量上分别与 float 进行运算,这就是所谓的 逐分量运算。GLSL 里,大部分涉及 vec,mat 的运算都是逐分量运算,但也并不全是。下文中就会讲到特例。 逐分量运算 是线性的,这就是说 vec 与 float 的运算结果是还是 vec。int 与 vec,mat 之间是不可运算的,因为 vec 和 mat 中的每一个分量都是 float 类型的,无法与 int 进行逐分量计算。下面枚举了几种 float 与 vec,mat 运算的情况:vec3 a = vec3(1.0, 2.0, 3.0);mat3 m = mat3(1.0);float s = 10.0;vec3 b = s * a; // vec3(10.0, 20.0, 30.0)vec3 c = a * s; // vec3(10.0, 20.0, 30.0)mat3 m2 = s * m; // = mat3(10.0)mat3 m3 = m * s; // = mat3(10.0)复制代码3、vec(向量) 与 vec(向量)运算:两向量间的运算首先要保证操作数的阶数都相同,否则不能计算。例如: vec3\*vec2 和 vec4+vec3 等等都是不行的。它们的计算方式是两操作数在同位置上的分量分别进行运算,其本质还是逐分量进行的,这和上面所说的 float 类型的逐分量运算可能有一点点差异,相同的是 vec 与 vec 运算结果还是 vec,且阶数不变。vec3 a = vec3(1.0, 2.0, 3.0);vec3 b = vec3(0.1, 0.2, 0.3);vec3 c = a + b; // = vec3(1.1, 2.2, 3.3);vec3 d = a * b; // = vec3(0.1, 0.4, 0.9);AB=[a1,1a1,2a2,1a2,2][b1,1b1,2b2,1b2,2]=[a1,1b1,1+a1,2b2,1a1,1b1,2+a1,2b2,2a2,1b1,1+a2,2b2,1a2,1b1,2+a2,2b2,2]4、vec(向量) 与 mat(矩阵):要保证操作数的阶数相同,且 vec 与 mat 间只存在乘法运算。 它们的计算方式和线性代数中的矩阵乘法相同,不是逐分量运算。vec2 v = vec2(10., 20.);mat2 m = mat2(1., 2., 3., 4.);vec2 w = m * v; // = vec2(1. * 10. + 3. * 20., 2. * 10. + 4. * 20.)vec2 v = vec2(10., 20.);mat2 m = mat2(1., 2., 3., 4.);vec2 w = v * m; // = vec2(1. * 10. + 2. * 20., 3. * 10. + 4. * 20.)5、mat(矩阵) 与 mat(矩阵):要保证操作数的阶数相同。在 mat 与 mat 的运算中,除了乘法是线性代数中的矩阵乘法外,其余的运算仍为逐分量运算。简单说就是只有乘法是特殊的,其余都和 vec 与 vec 运算类似。mat2 a = mat2(1., 2., 3., 4.);mat2 b = mat2(10., 20., 30., 40.);mat2 c = a * b; // mat2(1.*10.+3.*20.,2.*10.+4.*20.,1.* 30.+3.*40.,2.* 30.+4.*40.);mat2 d = a+b;// mat2(1.+10.,2.+20.,3.+30.,4.+40);结构体#结构体可以组合基本类型和数组来形成用户自定义的类型。在定义一个结构体的同时,你可以定义一个结构体实例。或者后面再定义。struct fogStruct { vec4 color; float start; float end; vec3 points[3]; // 固定大小的数组是合法的} fogVar;可以通过 = 为结构体赋值,或者使用 ==,!= 来判断两个结构体是否相等。fogVar = fogStruct(vec4(1.0,0.0,0.0,1.0),0.5,2.0);vec4 color = fogVar.color;float start = fogVar.start;构造函数#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 中只可以使用一维的数组。数组的类型可以是一切基本类型或者结构体。下面的几种数组声明是合法的:float floatArray[4];vec4 vecArray[2];float a[4] = float[](1.0,2.0,3.0,4.0);vec2 c[2] = vec2[2](vec2(1.0,2.0),vec2(3.0,4.0));数组类型内建了一个length()函数,可以返回数组的长度。lightPositions.length() // 返回数组的长度类型转换#glsl可以使用构造函数进行显式类型转换,各值如下:bool t= true;bool f = false;int a = int(t); //true转换为1或1.0int a1 = int(f);//false转换为0或0.0float b = float(t);float b1 = float(f);bool c = bool(0);//0或0.0转换为falsebool c1 = bool(1);//非0转换为truebool d = bool(0.0);bool d1 = bool(1.0);glsl中,没有隐式类型转换,原则上glsl要求任何表达式左右两侧(l-value),(r-value)的类型必须一致 也就是说以下表达式都是错误的:int a =2.0; //错误,r-value为float 而 lvalue 为int.int a =1.0+2;float a =2;float a =2.0+1;bool a = 0; vec3 a = vec3(1.0, 2.0, 3.0) * 2;精度限定#glsl在进行光栅化着色的时候,会产生大量的浮点数运算,这些运算可能是当前设备所不能承受的,所以glsl提供了3种浮点数精度,我们可以根据不同的设备来使用合适的精度.在变量前面加上 highp mediump lowp 即可完成对该变量的精度声明.lowp float color;varying mediump vec2 Coord;lowp ivec2 foo(lowp mat3);highp mat4 m;我们一般在片元着色器(fragment shader)最开始的地方加上 precision mediump float; 便设定了默认的精度.这样所有没有显式表明精度的变量 都会按照设定好的默认精度来处理.如何确定精度:变量的精度首先是由精度限定符决定的,如果没有精度限定符,则要寻找其右侧表达式中,已经确定精度的变量,一旦找到,那么整个表达式都将在该精度下运行.如果找到多个, 则选择精度较高的那种,如果一个都找不到,则使用默认或更大的精度类型.uniform highp float h1;highp float h2 = 2.3 * 4.7; //运算过程和结果都 是高精度mediump float m;m = 3.7 * h1 * h2; //运算过程 是高精度h2 = m * h1; //运算过程 是高精度m = h2 – h1; //运算过程 是高精度h2 = m + m; //运算过程和结果都 是中等精度void f(highp float p); // 形参 p 是高精度f(3.3); //传入的 3.3是高精度invariant关键字:由于shader在编译时会进行一些内部优化,可能会导致同样的运算在不同shader里结果不一定精确相等.这会引起一些问题,尤其是vertx shader向fragmeng shader传值的时候. 所以我们需要使用invariant 关键字来显式要求计算结果必须精确一致. 当然我们也可使用 #pragma STDGL invariant(all)来命令所有输出变量必须精确一致, 但这样会限制编译器优化程度,降低性能.#pragma STDGL invariant(all) //所有输出变量为 invariantinvariant varying texCoord; //varying在传递数据的时候声明为invariant限定符的顺序:当需要用到多个限定符的时候要遵循以下顺序:1.在一般变量中: invariant > storage > precision2.在参数中: storage > parameter > precision我们来举例说明:invariant varying lowp float color; // invariant > storage > precisionvoid doubleSize(const in lowp float s){ //storage > parameter > precision float s1=s;}修饰符#1、变量存储限定符限定符 描述(默认的可省略)只是普通的本地变量,可读可写,外部不可见,外部不可访问const 常量值必须在声明时初始化,它是只读的不可修改的varying 顶点着色器的输出,主要负责在 vertex 和 fragment 之间传递变量。例如颜色或者纹理坐标,(插值后的数据)作为片段着色器的只读输入数据。必须是全局范围声明的全局变量。可以是浮点数类型的标量,向量,矩阵。不能是数组或者结构体。uniform 一致变量。在着色器执行期间一致变量的值是不变的。与 const 常量不同的是,这个值在编译时期是未知的是由着色器外部初始化的。一致变量在顶点着色器和片段着色器之间是共享的。它也只能在全局范围进行声明。attribute 表示只读的顶点数据,只用在顶点着色器中。数据来自当前的顶点状态或者顶点数组。它必须是全局范围声明的,不能再函数内部。一个 attribute 可以是浮点数类型的标量,向量,或者矩阵。不可以是数组或则结构体centorid varying 在没有多重采样的情况下,与 varying 是一样的意思。在多重采样时,centorid varying 在光栅化的图形内部进行求值而不是在片段中心的固定位置求值。invariant (不变量)用于表示顶点着色器的输出和任何匹配片段着色器的输入,在不同的着色器中计算产生的值必须是一致的。所有的数据流和控制流,写入一个 invariant 变量的是一致的。编译器为了保证结果是完全一致的,需要放弃那些可能会导致不一致值的潜在的优化。除非必要,不要使用这个修饰符。在多通道渲染中避免 z-fighting 可能会使用到。2、函数参数限定符GLSL 允许自定义函数,但参数默认是以值形式(in 限定符)传入的,也就是说任何变量在传入时都会被拷贝一份,若想以引用方式传参,需要增加函数参数限定符。限定符 描述in 用在函数的参数中,表示这个参数是输入的,在函数中改变这个值,并不会影响对调用的函数产生副作用。(相当于C语言的传值),这个是函数参数默认的修饰符out 用在函数的参数中,表示该参数是输出参数,值是会改变的。inout 用在函数的参数,表示这个参数即是输入参数也是输出参数。其中使用 inout 方式传递的参数便与其他 OOP 语言中的引用传递类似,参数可读写,函数内对参数的修改会影响到传入参数本身。 eg:vec4 getPosition(out vec4 p){ p = vec4(0.,0.,0.,1.); return v4;}void doubleSize(inout float size){ size= size * 3.0 ;}内置变量和函数#内置变量#内置变量可以与固定函数功能进行交互。在使用前不需要声明。顶点着色器可用的内置变量名称 类型 描述gl_Color vec4 输入属性-表示顶点的主颜色gl_SecondaryColor vec4 输入属性-表示顶点的辅助颜色gl_Normal vec3 输入属性-表示顶点的法线值gl_Vertex vec4 输入属性-表示物体空间的顶点位置gl_MultiTexCoordn vec4 输入属性-表示顶点的第n个纹理的坐标gl_FogCoord float 输入属性-表示顶点的雾坐标gl_Position vec4 输出属性-变换后的顶点的位置,用于后面的固定的裁剪等操作。所有的顶点着色器都必须写这个值。gl_ClipVertex vec4 输出坐标,用于用户裁剪平面的裁剪gl_PointSize float 点的大小gl_FrontColor vec4 正面的主颜色的varying输出gl_BackColor vec4 背面主颜色的varying输出gl_FrontSecondaryColor vec4 正面的辅助颜色的varying输出gl_BackSecondaryColor vec4 背面的辅助颜色的varying输出gl_TexCoord[] vec4 纹理坐标的数组varying输出gl_FogFragCoord float 雾坐标的varying输出片段着色器的内置变量名称 类型 描述gl_Color vec4 包含主颜色的插值只读输入gl_SecondaryColor vec4 包含辅助颜色的插值只读输入gl_TexCoord[] vec4 包含纹理坐标数组的插值只读输入gl_FogFragCoord float 包含雾坐标的插值只读输入gl_FragCoord vec4 只读输入,窗口的x,y,z和1/wgl_FrontFacing bool 只读输入,如果是窗口正面图元的一部分,则这个值为truegl_PointCoord vec2 点精灵的二维空间坐标范围在(0.0, 0.0)到(1.0, 1.0)之间,仅用于点图元和点精灵开启的情况下。gl_FragData[] vec4 使用glDrawBuffers输出的数据数组。不能与gl_FragColor结合使用。gl_FragColor vec4 输出的颜色用于随后的像素操作gl_FragDepth float 输出的深度用于随后的像素操作,如果这个值没有被写,则使用固定功能管线的深度值代替内置函数#glsl提供了非常丰富的函数库,供我们使用,这些功能都是非常有用且会经常用到的. 这些函数按功能区分大改可以分成7类:通用函数:下文中的 类型 T可以是 float, vec2, vec3, vec4,且可以逐分量操作.方法 说明T abs(T x) 返回x的绝对值T sign(T x) 比较x与0的值,大于,等于,小于 分别返回 1.0 ,0.0,-1.0T floor(T x) 返回<=x的最大整数T ceil(T x) 返回>=等于x的最小整数T fract(T x) 获取x的小数部分T mod(T x, T y) T mod(T x, float y) 取x,y的余数T min(T x, T y) T min(T x, float y) 取x,y的最小值T max(T x, T y) T max(T x, float y) 取x,y的最大值T clamp(T x, T minVal, T maxVal) T clamp(T x, float minVal,float maxVal) min(max(x, minVal), maxVal),返回值被限定在 minVal,maxVal之间T mix(T x, T y, T a) T mix(T x, T y, float a) 取x,y的线性混合,x(1-a)+yaT step(T edge, T x) T step(float edge, T x) 如果 x