WebGL2.0从入门到精通-3、着色语言(1、数据类型及程序基本结构)

三、着色语言(Shader GLSL)

如何解读下面这个完整的顶点着色器程序呢?从着色语言开始讲起。

        1    #version 300 es
        2    uniform mat4 uMVPMatrix;                  //总变换矩阵
        3    layout (location = 3) in vec3 aPosition; //顶点位置
        4    layout (location = 2) in vec4 aColor;    //顶点颜色
        5    out vec4 vColor;                          //传递给片元着色器的输出变量
        6    void positionShift(){                     //根据总变换矩阵计算此次绘制顶点位置的方法
        7    gl_Position = uMVPMatrix * vec4(aPosition,1);
        8    }
        9    void main(){                              //主函数
        10   positionShift();                          //根据总变换矩阵计算此次绘制的顶点位置
        11   vColor = aColor;                          //将接收的颜色传递给片元着色器
        12   }

着色语言是一种高级图形编程语言,其源自广泛应用的 C 语言,同时具有 RenderMan 以及其他着色语言的一些优良特性。

着色语言主要包括以下一些特性:

  • 着色语言是一种高级过程语言(注意,不是面向对象的)
  • 对顶点着色器、片元着色器使用的是相同的语言,不进行区分。
  • 基于 C/C++ 的语法及流程控制。
  • 完美支持向量与矩阵的各种操作。
  • 通过类型限定符来管理输入与输出。
  • 拥有大量的内置函数来提供丰富的功能。

一、数据类型

着色语言与很多语言相似,支持多种原生数据类型以及构建数据类型。比如着色语言对浮点型(float)、布尔型(bool)、整型(int)、矩阵型(matrix)以及向量型(vec2、vec3等)都能很好地支持。在数据类型方面,着色语言可以支持标量、向量、采样器、结构体和数组等。

1、标量

标量与向量相比,只有大小没有方向,着色语言支持的标量有布尔型、整型和浮点型等。

1、布尔型

布尔型用来声明一个单独的布尔数。着色语言中布尔型只有true和false两个值。一般情况下,布尔类型的值由关系运算和逻辑运算产生。与C语言不同,在着色语言的流程控制中只能将布尔类型的值作为表达式。基本用法如下所示。

        bool b;             //声明一个布尔型变量
2、整型

着色语言中的整型较为特殊的方面是其应保证最少支持32位精度。实际开发时要注意,运算不要超出正常范围,否则很有可能产生溢出,产生不可预测的错误。着色语言中的整数也可以像C语言中的那样,不仅可以用十进制表示,有时为了使表达更加便捷还可以使用八进制或者十六进制等不同进制来表示,基本用法如下所示。

        1    int a = 7;          //用十进制表示的整型
        2    uint b=3u;          //无符号十进制
        3    int b = 034;        //以0开头的字面常量为八进制,代表十进制的28
        4    int c = 0x3C;       //以0x开头的字面常量为十六进制,代表十六进制的57
3、浮点数
        1    float f;              //声明一个浮点型的变量
        2    float g = 3.0;       //在声明变量的同时为变量赋初值
        3    float h, I;          //同时声明多个变量
        4    float j, k = 3.12, l;  //声明多个变量时,可以为其中某些变量赋初值
        5    float s=3e2;            //声明变量,并赋予指数形式的值,表示3乘以10的平方

2、向量

着色语言中,向量可以看作由同样类型的标量组成的数据类型。其中标量的基本类型也分为bool、int及float、double型 等类型。每个向量由两个、3个或4个相同的标量组成,如下:

向量类型 说明 向量类型 说明
vec2 包含2个浮点数的向量 bvec2 包含2个布尔数的变量
vec3 包含3个浮点数的向量 bvec3 包含3个布尔数的变量
vec4 包含4个浮点数的向量 bvec4 包含4个布尔数的变量
ivec2 包含2个整数的向量 uvec2 包含2个无符号整数的向量
ivec3 包含3个整数的向量 uvec3 包含3个无符号整数的向量
ivec4 包含4个整数的向量 uvec4 包含4个无符号整数的向量
dvec2 包含2个双精度浮点数的向量
dvec3 包含3个双精度浮点数的向量
dvec4 包含4个双精度浮点数的向量
        1    vec2 v2;                             //声明一个包含两个浮点数的向量
        2    ivec3 v3;                            //声明一个包含3个整数的向量
        3    uvec3 vu3    ;                       //声明一个包含3个无符号整数的向量
        4    bvec4 v4;                            //声明一个包含4个布尔数的向量

用一个向量表示颜色信息时,可以使用r、g、b、a这4个分量名,其分别代表红、绿、蓝、透明度4个色彩通道。

        1    aColor.r = 0.5;                         //给向量aColor的红色通道分量赋值
        2    aColor.g = 0.7;                         //给向量aColor的绿色通道分量赋值

用向量来表示物体的位置坐标时,可以使用此向量的x、y、z、w这4个分量名,其分别代表x轴、y轴、z轴分量及w值。

        1    aPosition.x = 57.1;                     //给向量aPosition的x轴分量赋值
        2    aPosition.z = 32.8;                     //给向量aPosition的z轴分量赋值

一个向量看作纹理坐标时,可以使用s、t、p、q这4个分量名,其分别代表纹理坐标的不同分量。

        1    aTexCoor.s = 0.24;                         //给向量aTexCoor的s分量赋值
        2    aTexCoor.t = 0.71;                         //给向量aTexCoor的t分量赋值

在访问向量中的各个分量时不但可以采用“.”加上不同分量名的方法,还可以将向量看作一个数组,指明向量名称并找出对应下标作为后缀来进行访问。具体用法如下。

        1    aColor[0]=0.6;                            //给向量aColor的红色通道分量赋值
        2    aPosition[2]=48.3;                        //给向量aPosition的z轴分量赋值
        3    aTexCoor[1]=0.34;                         //给向量aTexCoor的t分量赋值

3、矩阵

在3D场景的开发过程中,每个物体都要经过移位、旋转、缩放等变换。实质上这些变换是矩阵的运算。因此在3D场景的开发中,对矩阵的操作会很频繁。为了使用矩阵更加便捷,着色语言提供了对矩阵类型的支持,免去了构建矩阵的过程。

        1    mat2 m2;                                        //声明一个mat2类型的矩阵
        2    mat3 m3;                                        //声明一个mat3类型的矩阵
        3    mat4 m4;                                        //声明一个mat4类型的矩阵
        4    mat3x2 m5;                                      //声明一个mat3x2类型的矩阵

着色语言的矩阵类型是由多个向量按照列顺序进行组织的。在对矩阵进行访问时,可以将矩阵视为列向量的数组。例如:matrix为一个mat4类型的矩阵,matrix[2]代表该矩阵的第3列,其为一个vec4; matrix[2] [2]指的是第3列向量中的第3个分量。

4、采样器

采样器是着色语言中一种特殊的基本数据类型,专门用来执行纹理采样的相关操作。在片元着色器中,采样函数需要通过采样器变量来访问纹理单元。一般情况下,一个采样器变量代表一幅或一套纹理贴图。

在这里插入图片描述

需要注意的是,与前面介绍的几类变量不同,采样器变量不能在着色器中进行初始化。一般情况下采样器变量都用 uniform 限定符来修饰,从宿主语言(如C++、Java)接收传递至着色器的值。此外,采样器变量也可以用作函数的参数,但是作为函数参数时不可以使用 out 或 inout 修饰符来修饰。

5、结构体

        1    struct info{
    
                            //对名称为info的结构体进行声明
        2        vec3 color;                     //代表颜色的向量
        3         vec3 position;                 //代表位置的向量
        4         vec2 textureCoor;              //纹理坐标的向量
        5    }

自定义结构体声明完成后,如果开发人员需要使用此结构体,则同使用内建数据类型那样直接声明即可。具体用法如下面代码所示。

        info CubeInfo;                         //声明了一个info类型的变量CubeInfo

6、数组

对数组进行声明的方法主要有两种,具体代码如下。

(1)声明数组的同时指定数组的大小。

        vec3 position[20];                     //声明了一个包含20个vec3的数组,索引从0开始。

(2)在声明数组时,也可以不指定数组的大小,但是这时必须符合下列两种情况之一。

  • A、引用数组之前,要再次对数组进行声明,并指定数组的大小。具体代码如下所示。
        1    vec3 color[];                     //声明了一个大小不定的vec3型数组
        2    vec3 color[3];                    //再次声明该数组,并且指定大小
  • B、代码中访问数组的下标是常量,编译器会自动创建适当大小的数组,使得数组足够存储编译器看到的最大索引值对应的元素,代码如下。
        1    vec3 position[];                         //声明了一个大小不定的vec3型数组
        2    position[4] = vec3(4.0);                 //position需要为一个大小为5的数组
        3    position[16] = vec3(3.0);                //position需要为一个大小为17的数组

7、空类型

空类型使用void来表示,仅用来声明没有任何返回值的函数。例如在顶点着色器以及片元着色器中必须存在的main函数就是一个返回值为空类型的函数,代码如下所示。

        1   void main()                         //声明一个空返回值类型的main方法
        2   {  //函数的具体操作略 }

8、规则与注意事项

在着色语言中,变量应该按照下面的规则进行命名。

  • 由于系统中有很多内建变量都是以“gl_”作为开头的,因此用户自定义的变量不允许使用“gl_”作为开头,从而将自定义变量和内建变量区分出来。
  • 为自己的函数或变量命名时尽量采用有意义的拼写,除了一些局部变量外不要采用a、b、c 等名称。若一个单词不足以描述变量的用途,则可以用多个单词的组合,除第一个单词全小写外,其他每个单词的第一个字母大写。
        1    float a=56.3;         //声明了浮点变量a并赋初值
        2    float b=23.4;         //声明了浮点变量b并赋初值
        3    vec2 va=vec2(6.3,4.5); //声明了二维向量va并赋初值
        4    vec2 vb=vec2(a, b);    //声明了二维向量vb并赋初值
        5    vec3 vc=vec3(vb,95.5); //声明了三维向量vc并赋初值
        6    vec4 vd=vec4(va, vb);  //声明了四维向量vd并赋初值
        7    vec4 ve=vec4(3.2);    //声明了四维向量ve并赋初值,相当于vec4(3.2,3.2,3.2,3.2)
        8    vec3 vf=vec3(ve);     //声明vf并初始化,相当于ve3(3.2,3.2,3.2)舍弃了ve的第4个分量
        1    float a=6.3;                                 //声明了浮点变量a并赋初值
        2    float b=11.4;                                //声明了浮点变量b并赋初值
        3    float c=12.5;                                //声明了浮点变量c并赋初值
        4    vec3 va=vec3(2.3,2.5,3.8);
        5    vec3 vb=vec3(a, b, c);
        6    vec3 vc=vec3(vb.x, va.y,14.4);
        7    mat3 ma=mat3(1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0, c); //用9个字面常量初始化3×3矩阵
        8    mat3 mb=mat3(va, vb, vc);                //用3个向量初始化3×3矩阵
        9    mat3 mc=mat3(va, vb,1.0,2.0,3.0);      //给出多个向量和字面常量以初始化3×3矩阵
        10   mat3 md=mat3(2.0) ;                    //给出1个字面常量以初始化3×3矩阵
        11   mat4x4 me=mat4x4(3.0); //等价于mat4x4(3.0,0.0,0.0,0.0,0.0,3.0,0.0,0.0,0.0,0.0,
              3.0,0.0,0.0,0.0,0.0,3.0)
        12   mat3x3 mf=mat3x3(me); //等价于mat3x3(3.0,0.0,0.0,0.0,3.0,0.0,0.0,0.0,3.0)
        13   vec2 vd=vec2(a, b);
        14   mat4x2 mg=mat4x2(vd, vd, vd, vd);
        15   mat2x3 mh=mat2x3(mg); //等价于mat2x3(6.3,11.4,0.0, 6.3,11.4,0.0)
        16   mat4x4 mj=mat4x4(mf); //等价于mat4×4(3.0,0.0,0.0,0.0,0.0,3.0,0.0,0.0,0.0,0.0,
              3.0,0.0, 0.0,0.0,0.0,1.0)
初始化规则

1、常用初始化方式:变量可以在声明的时候就进行初始化。

        int a=5, b=6, c;                     //声明了整型变量a、b与c,同时为a与b变量赋初值

2、用 const 限定符修饰的变量必须在声明的时候进行初始化。

        const float k=3.0;                 //在声明的时候初始化

3、全局的输入变量、一致变量以及输出变量在声明的时候一定不能进行初始化。

        1    in float angleSpan;   //不可对全局输入变量进行初始化
        2    uniform int k;        //不可对一致变量进行初始化
        3    out vec3 position;    //不可对全局输出变量进行初始化

矩阵初始化:

        1    vec2 d=vec2(1.0,2.0);  //d的分量值分别为1.0、2.0
        2    mat2 e=mat2(d, d);      //e的第1列和第2列均为(1.0、2.0)
        3    mat3 f=mat3(e);        //将矩阵e放到矩阵f的左上角,右下角剩余对角线元素的值为1,其余为0
        4    mat4x2 g=mat4x2(d, d, d, d);  //声明一个mat4*2矩阵
        5    mat2x3 h=mat2x3(g); //将矩阵g左上角的2×2个元素值赋值给h中的对应元素,h矩阵的最后一行为0,0
        6    mat3 myMat3 = mat3(1.0, 0.0, 0.0,        //矩阵 myMat3第1列的值
        7                             0.0, 1.0, 0.0,   //矩阵 myMat3第2列的值
        8                             0.0, 1.0, 1.0);  //矩阵 myMat3第3列的值

在着色器语言中矩阵元素的存储顺序以列为主,即矩阵由列向量组成。因此,当使用矩阵的构造函数时,矩阵元素将会按照矩阵中列的顺序依次被参数赋值。

运算符:

运算符列表(下图)中各个运算符的优先级是按照从左到右,再从上到下的顺序进行排列的。在进行实际开发时一定要注意运算符的优先级,否则会出现难以预料的后果。

在这里插入图片描述

9、限定符

着色器对变量也有很多可选的限定符。这些限定符中的大部分只能用来修饰全局变量。

在这里插入图片描述

        1    uniform mat4 uMVPMatrix;    //声明一个用uniform修饰的mat4类型的矩阵
        2    in vec3 aPosition;          //声明一个用in修饰的vec3类型的向量
        3    out vec4 aaColor;            //声明一个用out修饰的vec4类型的向量
        4    const int lightsCount = 4;  //声明一个用const修饰的整型的常量

限定符在使用时应该放在变量类型之前,且使用in、uniform以及out限定符来修饰的变量必须为全局变量。同时要注意的是,着色语言中没有默认的限定符,因此如果有需要,则必须为全局变量明确指定需要的限定符。

顶点着色器输入变量
           in vec3 aPosition;  //顶点位置
           in vec3 aNormal;    //顶点法向量
        1    this.vertexData= [3.0,0.0,0.0,0.0,0.0,0.0,3.0,0.0];   //初始化顶点坐标数据
        2    this.vertexBuffer=gl.createBuffer();                   //创建顶点坐标数据缓冲
        3    gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);     //绑定顶点坐标数据缓冲
        4    //将顶点坐标数据送入缓冲
        5    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(this.vertexData), gl.STATIC_DRAW);
        6    //启用顶点坐标数据
        7    gl.enableVertexAttribArray(gl.getAttribLocation(this.program, "aPosition"));
        8    //给管线指定顶点坐标数据
        9    gl.vertexAttribPointer(
        10   gl.getAttribLocation(this.program, "aPosition"),       //获得aPosition的位置
        11   3,                                                      //每组数据中的分量个数
        12   gl.FLOAT, false,0,0);
  • 第1行为初始化顶点坐标数据的代码。在vertexData数组中每3个数据为一组,分别为顶点的x、y、z坐标。此数组中的数据没有特殊性,读者可以更改。
  • 第2~3行为创建和绑定顶点坐标数据缓冲的相关代码,它为上传数据做好了准备。
  • 第4~7行为将顶点坐标数据送入缓冲并启用的代码。gl.bufferData()为将数据传入缓冲对象中的方法。❑
  • 第8~12行使用gl.vertexAttribPointer()方法为管线指定了顶点坐标的数据。此方法参数较多,较为复杂。第一个参数为变量a_Position在着色器中的地址,通常用gl.getAttribLocation方法进行查询。

从上述代码中可以看出,**主要工作分为3步,即创建缓冲、绑定缓冲、将数据传入缓冲中。**上述工作往往不是在一个方法中完成的。

顶点着色器的 in 变量不可以声明数组,同样输入变量也不可以声明结构体对象。此外,顶点着色器的 in 变量不像一致变量一样能通过打包传送数据,因此最好使用 vec4 的整数倍进行送入,以提高效率。

片元着色器输入变量

在片元着色器中,in/centroid in限定符可以修饰的类型包括有符号或无符号的整型标量、整型向量、浮点数标量、浮点数向量、矩阵变量、数组变量或结构体变量。然而,当片元着色器中in/centroid in变量的类型为有符号或无符号整型标量或整型向量时,变量必须使用flat限定符来修饰。

        1    in vec3 vPosition;              //接收从顶点着色器传递过来的顶点位置数据
        2    centroid in vec2 vTexCoord;    //接收从顶点着色器传递过来的纹理坐标数据
        3    flat in vec3 vColor;            //接收从顶点着色器传递过来的颜色数据
uniform 限定符

uniform为一致变量限定符,一致变量指的是在由同一组顶点组成的单个3D物体中所有顶点都是相同的量。uniform变量可以用在顶点着色器或片元着色器中,用来修饰所有的基本数据类型。与in变量类似,一致变量的值也是从宿主程序传入的。下面的代码片段给出了在顶点或片元着色器中正确使用uniform限定符的情况。

        1    uniform mat4 uMVPMatrix;                 //总变换矩阵
        2    uniform mat4 uMMatrix;                   //基本变换矩阵
        3    uniform vec3 uLightLocation;             //光源位置
        4    uniform vec3 uCamera;                    //摄像机位置

将一致变量的值从宿主程序传入渲染管线的相关代码如下。

        1    var uMVPMatrixHandle;    //总变换矩阵一致变量的引用
        2    //获取着色器程序中总变换矩阵一致变量的引用
        3    muMVPMatrixHandle =gl. getUniformLocation (this.program, "uMVPMatrix");
        4    //通过一致变量的引用将一致变量值传入渲染管线
        5    gl.uniformMatrix4fv(uMVPMatrixHandle, false, new Float32Array(ms.getFinalMatrix()));

从上述代码中可以看出,将一致变量的值送入渲染管线比较简单,主要包括两个步骤,分别为获取着色器程序中一致变量的引用以及调用uniformMatrix4fv等方法将对应一致变量的值送入渲染管线。

uniform关键字出现的目的就是为了javascript可以通过相关的WebGL API给着色器变量传递数据,比如传递一个光源的位置数据、一个光源的方向数据、一个光源的颜色数据、一个用于顶点变换的模型矩阵、一个用于顶点变换的视图矩阵…

不过要注意如果是顶点相关的变量,比如顶点位置、顶点颜色等顶点数据相关变量不能使用关键字uniform去声明,主要是顶点的数据往往不是一个,通常有很多个顶点,而且这些顶点都要逐顶点执行main函数中的程序,所以为了声明顶点数据相关的变量,着色器语言规定了一个新的关键字attribute

javascript可以给顶点着色器的变量传递数据,也可以给片元着色器的变量传递数据,也就是说uniform关键字既可以在顶点着色器中使用,也可以在片元着色器中使用。只要注意uniform关键字声明变量需要在主函数main之前声明。

attribute 变量

attribute关键字通常用来声明与顶点数据相关的变量,比如顶点位置坐标数据、顶点颜色数据、顶点法向量数据…

顶点着色器中通过attribute关键字声明的顶点变量,javascript代码可以通过相关的WebGL API把顶点的数据传递给着色器中相应的顶点变量。

因为javascript没必要给片元着色器传递顶点数据,所以规定attribute关键字只能在顶点着色器中声明变量使用。只要注意attribute关键字声明顶点变量代码位于主函数main之外就可以。

// attribute声明顶点位置变量
attribute vec4 position;
// attribute声明顶点颜色变量
attribute vec4 a_color;
// attribute声明顶点法向量
attribute vec4 normal;
// 与顶点相关的浮点数
attribute float scale;

同一个顶点着色器通常需要处理是一批顶点数据,一个顶点可能会有多种数据,比如顶点位置、颜色、法向量,还有其它自定义的,比如attribute float scale;,声明了一个scale变量。

// attribute声明顶点位置变量
attribute vec4 position;
// 与顶点相关的浮点数
attribute float scale;
void main() {
  // 每个顶点的x坐标乘以该顶点对应的一个系数scale
  gl_Position = vec4(position.x*scale,position.y,position.z,1.0);
}

attribute 就是属性信息,属性信息可以是布尔、字符串、数字等等。

但顶点数据就是位置信息就是数字。

例如一个点的位置为(3,2)这属于顶点数据,这个点可能是红色、大小为 10 等这些属于 attribute

varying 变量

attribute vec4 a_color;在顶点着色器中声明了一个顶点颜色变量,如果想在片元着色器中获得顶点颜色插值计算以后的数据,需要同时在顶点着色器和片元着色器中执行varying vec4 v_color;,也就是在顶点、片元两个着色器代码中都需要通过关键字varying声明一个新变量v_color,最后再顶点着色器中执行v_color = a_color;即可

顶点着色器

attribute vec4 a_color;// attribute声明顶点颜色变量
varying vec4 v_color;//varying声明顶点颜色插值后变量
void main() {
  //顶点颜色插值计算
  v_color = a_color;
}

片元着色器

// 接收顶点着色器中v_color数据
varying vec4 v_color;
void main() {
  // 插值后颜色数据赋值给对应的片元
  gl_FragColor = v_color;
}

varying类型变量主要是为了完成顶点着色器和片元着色器之间的数据传递和插值计算。比如在一个WebGL程序中通过三个顶点绘制一个彩色三角形,三个顶点的位置坐标定义了一个三角形区域,这个三角形区域经过片元着色器处理后会得到由一个个片元或者说像素组成的三角形区域,在片元化的过程中,顶点的颜色数据也会进行插值计算,插值计算之前每个顶点对应一个颜色,插值计算之后,每个片元对应一个颜色值,通过varying关键字就可以在片元着色器中获得插值后的颜色数据,然后赋值给片元。

<!-- 顶点着色器源码 -->
<script id="vertexShader" type="x-shader/x-vertex">
  //attribute声明vec4类型变量apos
  attribute vec4 apos;
  // attribute声明顶点颜色变量
  attribute vec4 a_color;
  //varying声明顶点颜色插值后变量
  varying vec4 v_color;
  void main() {
    // 顶点坐标apos赋值给内置变量gl_Position
    gl_Position = apos;
    //顶点颜色插值计算
    v_color = a_color;
  }

</script>
<!-- 片元着色器源码 -->
<script id="fragmentShader" type="x-shader/x-fragment">
  // 所有float类型数据的精度是lowp
  precision lowp float;
  // 接收顶点着色器中v_color数据
  varying vec4 v_color;
  void main() {
    // 插值后颜色数据赋值给对应的片元
    gl_FragColor = v_color;
  }

</script>
顶点着色器的输出变量

在顶点着色器中可以使用out或centroid out限定符修饰全局变量,以向渲染管线后继阶段传递当前顶点的数据。

在顶点着色器中,out/centroid out限定符只能用来修饰浮点型标量、浮点型向量、矩阵变量、有符号或无符号的整型标量或整型向量、数组变量及结构体变量。

当顶点着色器中out/centroid out变量的类型为有符号或无符号的整型标量或整型向量时,变量也必须使用flat限定符来修饰。

下图给出了默认情况下out变量的工作原理。

在这里插入图片描述

从图中可以看出,在默认情况下,顶点着色器在每个顶点上都对out变量vPosition进行赋值。接着在片元着色器中接收in变量vPosition的值时得到的并不是由某个顶点赋予的特定值,而是根据片元所在位置及图元中各个顶点的位置进行插值计算产生的值。

图中顶点1、2、3的vPosition值分别为vec3(0.0,7.0,0.0)、vec3(-5.0,0.0,0.0)、vec3(5.0,0.0,0.0),插值后片元a的vPosition值为vec3(1.27,5.27,0.0)。这个值是根据3个顶点对应的着色器给vPosition赋的值、3个顶点位置及此片元位置由管线插值计算而得到的。

从上述介绍中可以看出,光栅化后产生了多少个片元,就会插值计算出多少套in变量。同时,渲染管线就会调用多少次片元着色器。一般情况下在对一个3D物体进行渲染时,片元着色器执行的次数会大大超过顶点着色器。因此,GPU硬件中配置的片元着色器的硬件数量往往多于顶点着色器的硬件数量。这些硬件单元的并行执行可以提高渲染速度。

下面的代码片段给出了在顶点着色器中正确使用out/centroid out限定符的情况。

        1    out vec4 ambient;                    //环境光out变量
        2    out vec4 diffuse;                    //散射光out变量
        3    centroid out vec2 texCoor;          //纹理坐标out变量
        4    invariant centroid out vec4 color;  //颜色值out变量
片元着色器的输出变量

在片元着色器中只能使用out限定符来修饰全局变量,而不能使用centroid out限定符。

片元着色器中的out变量一般指的是由片元着色器写入计算片元颜色值的变量,一般需要在片元着色器的最后对其进行赋值,并将其送入渲染管线的后继阶段进行处理。

在片元着色器中,out限定符只能用来修饰浮点型标量、浮点型向量、有符号或无符号的整型量或整型向量及数组变量,不能用来修饰其他类型的变量。

下面的代码片段给出了在片元着色器中正确使用out限定符的情况。

        1    out vec4 fragColor;    //输出的片元颜色
        2    out uint luminosity;

对于顶点着色器而言,一般是既声明out变量,又对out变量进行赋值以传递给片元着色器。而片元着色器中声明in变量以接收顶点着色器传过来的值,是不可以对in变量进行赋值的。并且WebGL 2.0中片元着色器的内建输出变量gl_FragColor(此内建变量在WebGL 1.0中几乎总要用到)已不存在,需要自己声明out(vec4)变量,用声明的out变量替代gl_FragColor内建变量。

插值限定符

插值(interpolation)限定符主要用于控制顶点着色器传递到片元着色器的数据的插值方式。插值限定符包含smooth、flat两种。

在这里插入图片描述

若使用插值限定符,则该限定符应该在in、centroid in、out或centroid out之前使用,且只能用来修饰顶点着色器的out变量与片元着色器中对应的in变量。当未使用任何插值限定符时,默认的插值方式为smooth。

如果在顶点着色器的out变量之前含有flat限定符,则传递到后继片元着色器中对应in变量的值不是在光栅化阶段插值产生的,一般是由图元中与最后一个顶点对应的顶点着色器对此out变量所赋的值来决定的。

一致块

多个一致变量的声明可以通过类似结构体形式的接口块来实现,该形式的接口块又称为一致块(uniform block)。一致块的数据是通过缓冲对象送入渲染管线的,以一致块形式批量传送数据比单个传送效率高,其基本语法为:

        [<layout 限定符>] uniform一致块名称 {<成员变量列表>} [<实例名>]
        1    uniform MatrixBlock{                //一致块
        2    mat4 uMVPMatrix;                    //块成员变量
        3    } mb;
        4    gl_Position = mb.uMVPMatrix * vec4(aPosition,1); //根据总变换矩阵计算此次绘制的顶点位置
layout 限定符

layout限定符是从WebGL 2.0开始出现的,主要用于设置变量的存储索引(即引用)值

        1    layout (location=0) in vec3 aPosition; // aPosition 输入变量的引用值为0
        2    layout (location=1) in vec4 aColor;    // aColor 输入变量的引用值为1
        1    layout (location=0) out vec4 fragColor;    //此输出变量写入到0号绘制缓冲
        2    layout (location=1) out vec4 colors[2];    //此输出变量写入到1号绘制缓冲

10、流程控制

着色语言提供了5种流程控制方式,分别由if-else条件语句、switch-case-default条件语句、while(do-while)循环语句、for循环语句以及break与continue循环控制语句。

11、函数的声明与使用

        <返回类型> 函数名称 ([<参数序列>]){ /*函数体*/}
  • “in”修饰符,用其修饰的参数为输入参数,仅使函数接收外界传入的值。若某个参数没有明确给出用途修饰符,则等同于使用了“in”修饰符。

  • “out”修饰符,用其修饰的参数为输出参数,在函数体中对输出参数赋值可以将值传递到调用它的外界变量中。对于输出参数,要注意的是在调用时不可以使用字面常量。

  • “inout”修饰符,用其修饰的参数为输入/输出参数,它们具有输入与输出两种参数的功能。

        1    void pointLight(in vec4 x, out vec4 y){}
        2    void pointLight(in vec4 x, out ivec4 y){}             //参数类型不同
        3    void pointLight(in vec4 x, out vec4 y, out vec4 z){}  //参数个数不同

12、片元着色器中浮点变量精度的指定

片元着色器在使用浮点相关类型的变量时与顶点着色器中的有所不同,在顶点着色器中直接声明然后使用即可,而在片元着色器中必须指定精度,若不指定精度,则可能会引起编译错误。指定精度的方法如下面的代码片段所示。

        1    lowp float color;                 //指定名称为color的浮点型变量精度为lowp
        2    varying mediump vec2 Coord;      //指定名称为Coord的vec2型变量精度为mediump
        3    highp mat4 m;                     //指定名称为m的mat4型变量精度为highp

精度共有lowp、mediump及highp 3种选择。这3种选择分别代表低、中、高精度等级,在不同的硬件中实现可能会有所不同。一般情况下,使用mediump即可。

如果在开发中同一个片元着色器的浮点相关类型的变量都选用同一种精度,则可以指定整个着色器的浮点相关类型的默认精度,具体语法如下。

        precision <精度> <类型>;
  • 精度可以选择lowp、mediump及highp中的一个。
  • 类型一般为float,这不仅表示为浮点标量类型float指定了精度,还表示对浮点类型相关的向量、矩阵也指定了默认精度。因此一般开发中经常将片元着色器的第一句写为“precision mediump float; ”。

13、程序的基本结构

前面介绍了着色语言中很多独立的基本知识。

着色器程序一般由3部分组成,主要包括全局变量声明、自定义函数、main函数。下面的代码片段给出了一个完整的顶点着色器程序。

        1    #version 300 es
        2    uniform mat4 uMVPMatrix;                  //总变换矩阵
        3    layout (location = 3) in vec3 aPosition; //顶点位置
        4    layout (location = 2) in vec4 aColor;    //顶点颜色
        5    out vec4 vColor;                          //传递给片元着色器的输出变量
        6    void positionShift(){                     //根据总变换矩阵计算此次绘制顶点位置的方法
        7    gl_Position = uMVPMatrix * vec4(aPosition,1);
        8    }
        9    void main(){                              //主函数
        10   positionShift();                          //根据总变换矩阵计算此次绘制的顶点位置
        11   vColor = aColor;                          //将接收的颜色传递给片元着色器
        12   }
  • 第1行声明使用的着色语言为3.0版本,每个着色器开始都必须使用该语句来声明着色语言版本。
  • 第2~5行为全局变量的声明,根据具体情况,代码可能会有增加或减少。
  • 第6~8行为自定义的函数,这一部分根据需要可能没有,也可能有很多不同的函数。
  • 第7行的gl_Position是顶点着色器中的内建变量。
  • 第9~12行为主函数main,这是每个着色器里面都必须有的部分。

每个着色器都必须在着色器程序的第1行通过“#version 300 es”语句声明使用3.0版的着色语言。如果没有该语句,则表示着色语言的版本是2.0。另外,与很多高级语言不同,着色器程序要求被调用的函数必须在调用之前声明,且在自己开发的着色器中自己开发的函数不可以递归调用。如上述代码中首先在第6行声明了positionShift函数,然后在第10行进行调用。

WebGL的着色器代码分为顶点着色器代码和片元着色器代码两部分,顶点着色器代码会在GPU的顶点着色器单元执行,片元着色器代码会在GPU的片元着色器单元执行,在WebGL渲染管线流程中,或者说GPU的渲染流程中,顶点着色器代码先执行处理顶点,得到一系列片元,然后再执行片元着色器代码处理片元。

顶点着色器和片元着色器代码都有一个唯一的主函数main(),attribute、varying和uniform类型的变量需要在main函数之外声明,在main函数中通常编写,逐片元或逐顶点处理的代码。

在着色器中通过attribute和uniform关键字声明的变量,需要通过javascript代码传递相关的数据。比如通过关键字attribute声明的顶点位置坐标数据,可以通过javascript调用WebGL相关API传递顶点数据。

// attribute、varying和uniform关键字声明变量的位置
void main(){
  ...
  // 顶点着色代码或者片元着色器代码
  ...
}

本章参考如下:

《WebGL 3D 开发实战详解》

WebGL 零基础入门教程

猜你喜欢

转载自blog.csdn.net/yinweimumu/article/details/128824816