《WebGL 编程指南》笔记 —— 第六章 OpenGL ES着色器语言

本章主要内容:
(1)数据、变量和变量类型。
(2)矢量、矩阵、结构体、数组、采样器(纹理)
(3)运算、程序流、函数
(4)attribute、uniform和varying变量
(5)精度限定词
(6)预处理和指令

  1. WebGL并不支持GLSL ES 1.00的所有特性。实际上,它支持的是1.00版本的一个子集,其中只包括WebGL需要的那些核心特性。

  2. GLEL ES编程语言是在OpenGL着色器语言(GLSL)的基础上,删除和简化一部分后形成的,降低了硬件消耗,减少了性能开销。

  3. 基础:
    (1)程序式大小写敏感
    (2)每一个语句都应该以一个英文分号结束

  4. 执行次序
    从main函数开始执行。
    着色器程序有且仅有一个main()函数,而且该函数不能接受任何参数。
    main函数前的void关键字表示这个函数不返回任何值。

  5. 注释
    单行注释: // int kp = 496;
    多行注释: /* haha */

  6. 数据值类型(数值和布尔值)
    GLSL支持两种数据值类型
    (1)数值类型:整数(没有小数点)和浮点数(有小数点)
    (2)布尔值类型:true 和 false
    不支持字符串类型

  7. 变量
    规则:
    (1)只包括a-z,A-Z,0-9和下划线_
    (2)变量名的首字母不能是数字
    (3)不能是关键字和保留字,但是变量名的一部分可以是它们
    (4)不能以gl_webgl_,或_webgl_开头,这些前缀已经被OpenGL ES保留了

GLSL ES关键字
GLSL ES保留字
  1. GLSL ES是强类型语言
    (1)GLSL ES要求具体指明变量的数据类型: <类型> <变量名>
    如: vec4 a_Position
    (2)定义函数时,必须指定函数的返回值
    (3)在进行赋值操作(=)的时候,等号左右两侧的数据类型也必须一样,否则就会出错

  2. 基本类型

GLSL的基本类型

为变量指定类型有利于WebGL系统检查代码错误,提高程序的运行效率。
如: float klimt // 浮点数变量

  1. 赋值和类型转换
    = 用于赋值,赋值时要保证左侧变量的类型和右侧的值类型一致
float f2 = 8.0;

可以使用内置函数进行类型转换,如:

float f3 = float(8);
类型转换内置函数
  1. 运算符
基本类型的运算符

说明:

[1] 在进行逻辑与(&&)运算时,只有第一个表达式的计算值为true时才会计算第二个表达式。同样,在进行逻辑或(||)运算时,只有第一个表达式的值为false时才会计算第二个表达式。

[2] 逻辑异或(^^)运算的含义是:只有当左右两个表达式中有且仅有一个为true时,运算结果才是true,否则为false。

  1. 矢量和矩阵
    (1)矢量和矩阵类型的变量都包含多个元素,每个元素是一个数值(整型数、浮点数和布尔值)
    矢量将这些元素排成一列,可以用来表示顶点坐标或颜色值等,而矩阵将元素划分成行和列,可以用来表示变换矩阵。
6-6.png

(2)赋值和构造
(a) = 等号用于赋值,如:vec4 position = vec4(1.0, 2.0, 3.0, 4.0);
(b)构造函数:专门创建指定类型的变量的函数,构造函数的名称和其创建的变量类型名称总是一致的。

(3)矩阵构造函数
(a)想矩阵构造函数中传入矩阵的每一个元素的数值来构造矩阵,注意传入值的顺序必须是列主序的

(b)向矩阵构造函数中传入一个或多个矢量,按照列主序使用矢量里的元素值来构造矩阵。

// 使用两个vec2对象来创建mat2对象
vec2 v2_1 = vec2(1.0, 3.0);
vec2 v2_2 = vec2(2.0, 2.0);
mat2 m2_1 = mat2(v2_1, v2_2);   // 1.0  2.0
                                //  3.0  4.0

// 使用一个vec4对象来创建mat2对象
vec4 v4 = vec4(1.0, 3.0, 2.0, 4.0);
mat2 m2_2 = mat2(v4);     // 1.0  2.0
                          //  3.0  4.0

(c)向矩阵构造函数中出阿奴矢量和数值,按照列主序使用矢量里的元素值和直接传入的数值来构造矩阵

// 使用两个浮点数和一个vec2对象来创建mat2对象
mat2 m2 = mat2(1.0, 3.0, v2_2);      // 1.0  2.0
                                     //  3.0  4.0

(d)向矩阵构造函数中传入单个数值,这样将生成一个对角线上元素都是该数值,其他元素为0.0的矩阵

mat4 m4 = mat4(1.0);    // 1.0  0.0  0.0  0.0
                        // 0.0  1.0  0.0  0.0
                        // 0.0  0.0  1.0  0.0
                        // 0.0  0.0  0.0  1.0

与矢量构造函数类似,如果传入的数值的数量大于1,有没有达到矩阵元素的数量,就会出错

mat4 m4 = mat4(1.0, 2.0, 3.0);    // 错误。mat4对象需要16个元素

(3)访问元素
为了访问矢量或矩阵中的元素,可以使用.[]运算符
(a).运算符

分量名

任何适量的x,r或s分量都会返回第一个分量,y,g,t分量都会返回第二个分量。
如:

vec3 v3 = vec3(1.0, 2.0, 3.0);    // 将v3设为(1.0, 2.0, 3.0)
float f;

f = v3.x;  // 设f为 1.0
f = v3.y;  // 设f为 2.0
f = v3.z;  // 设f为 3.0

f = v3.r;  // 设f为 1.0
f = v3.s;  // 设f为 1.0

将(同一个集合的)多个分量名共同置于点运算符后,就可以从矢量中同时抽取出多个分量。这个过程乘坐混合(swizzling)
如: v2 = v3.xz
此时的多个分量必须属于同一个集合,比如说,你不能使用v3.was

(b)[]运算符
矩阵中的元素从下标0开始按照列主序读取。
限制:[]中只能出现的索引值必须是常量索引值

常量索引值定义如下:
(a)整型字面量(0或1)
(b)用 const 修饰的全局变量或局部变量。不包括函数参数。
(c)循环索引
(d)由前述三条中的项组成的表达式

const int index = 0  // const 关键字表示变量是只读的
vec4 v4a = m4[index]  // 同m4[0]相同

注意,你不能使用未经const修饰的变量作为索引值,因为它不是一个常量索引值(除非它是循环索引)。

int index1 = 0
vec4 v4c = m4[index2]    // 错误:index不是常量索引

(4)运算符
对于矢量和矩阵,只可以使用比较运算符中的==!=,不可以使用><>=<=
如果想要比较矢量和矩阵的大小,应该是用内置函数,比如lessThan()
如果你想逐分量比较,可以使用内置的函数equal()notEqual()

矢量和矩阵可用的运算符
  1. 矢量和浮点数的运算

(1) 矢量运算

(2) 矩阵和浮点数的运算

(3)矩阵右乘矢量

(4) 矩阵左乘矢量

(5)矩阵与矩阵相乘

  1. 结构体
    (1)结构体:用户自定义的类型,使用关键字 struct,将已存在的类型聚合到一起,就可以定义为结构体。如:
struct light {    //  定义结构体light
  vec4 color;    // 光的颜色
  vec4 position;    // 广元位置
} 
light 11, 12;    // 声明了light类型的变量11和12

也可以在定义结构体的同时声明该结构体类型类型的变量,如:

struct light {    //  定义结构体和定义变量同时进行
  vec4 color;    // 光的颜色
  vec4 position;    // 广元位置
} 11;    // 该结构体类型的变量11

(2)赋值和构造
结构体有标准的构造函数,其名称与结构体名一致。构造函数的参数的顺序必须与结构体定义中的成员顺序一致。

结构体构造函数的使用方法

(3)访问成员
在结构体变量名后跟点运算符(.),然后再加上成员名,就可以访问变量的成员。如:

vec4 color = 11.color;
vec3 position = 11.position;
  1. 数组
    (1)
    ELSL ES 只支持一维数组,而且数组对象不支持pop()和push()等操作,创建数组时也不需要使用new运算符。
    声明数组,只需要在变量名后加上中括号和数组长度,如:
float floatArray[4];    // 声明含有4个浮点数元素的数组
vec4 vec4Array[2];    //  声明含有两个vec4对象的数组

数组的长度必须是大于0的整型常量表达式,定义如下:
(a)整型字面量(如0或1)
(b)用const限定字修饰的全局变量或局部变量,不包括函数参数
(c)由前述两条中的项组成的表达式

举例:

int size = 4;
vec4 vec4Array[size];    // 错误。如果第一行为const int size = 4;则不会报错

注意,你不可以用const限定字来修饰数组本身。

只有整型常量表达式和uniform变量可以被用作数组的索引值。
数组不能在声明时被一次性地初始化,而必须显式地对每个元素进行初始化。如:

vec4Array[0] = vec4(4.0, 3.0, 6.0, 1.0);
vec4Array[1] = vec4(3.0, 2.0, 0.0, 1.0);

数组本身只支持[]运算符,但数组的元素能够参与其自身类型支持的任意运算。如:

// 将floatArray的第二个参数乘以3.14
float f = floatArray[1] * 3.14;
// 将vec4Array的第一个参数乘以vec4(1.0, 2.0, 3.0 ,4.0)
vec4 v4 = vec4Array[0] * vec4(1.0, 2.0, 3.0 ,4.0);
  1. 取样器(纹理)
    必须通过取样器(sampler)类型变量访问纹理。
    有两种基本类型的取样器类型:sampler2DsamplerCube
    取样器变量只能是uniform变量,或者需要访问纹理的函数,如texture2D()函数的参数,如:
uniform sampler2D u_Sampler;

只有纹理单元编号可以给取样器变量,而且必须使用gl.uniformli()来进行赋值。

除了===!=,取样器变量不可以作为操作数参与运算。

取样器变量受到着色器支持的纹理单元的最大数量限制。

着色器中取样器类型变量的最小数量

mediump是一个精度限定字

  1. 运算符优先级
  1. 程序流程控制:分支和循环
    (1)if 和 if-else


    if语句格式

如:

if (distance < 0.5) {
  gl_fragColor = vec4(1.0, 0.0, 0.0, 1.0);
} else {
  gl_fragColor = vec4(0.0, 1.0, 0.0, 1.0);
}

(2)for语句

for语句格式

如:

for (int i = 0; i < 3; i++) {
  sum += i;
}

注意:循环变量i只能在初始化表达中定义,条件表达式可以为空,如果这样做,空的条件表达式返回true

for语句的其他限制:
(a)只允许有一个循环变量,循环变量只能是intfloat类型。
(b)循环表达式必须是以下的形式:i++,i--,i+=常量表达式或i-=常量表达式
(c)条件表达式必须是循环变量与整型常量的比较
(d)在循环体内,循环变量不可被赋值
这些限制的存在是为了使编译器就能够对for循环进行内联展开

(3)continue、break和discard语句
(a)continue终止包含该语句的最内层循环和执行循环表达式(递增/递减循环变量),然后执行下一次循环
(b)break中止包含该语句的最内层循环,并不在继续执行循环。

如:

// continue case
for (int i = 0; i < 10; i++) {
  if (i == 8) {
    continue;    // 跳过循环体余下的部分,继续下次循环
  }
  // 当i==8时,不会执行到这里
}

// break case
for (int i = 0; i < 10; i++) {
  if (i == 8) {
    break;    // 跳出for循环
  }
  // 当i>=8时,不会执行这里
}
// 当i==8时,执行这里

关于discard,它只能在片元着色器中使用,表示放弃当前片元直接处理下一片元。

  1. 函数
    (1)
函数语句格式

可以没有return语句,但是返回类型必须是void
也可以将自己定义的结构体指定为返回类型,但是结构体的成员中不能有数组。

示例:

// RGBA颜色值转为亮度值函数
float luma(vec4, color) {
  return 0.2126 * color.r +  0.7162 * color.g +  0.0722 * color.b;
}

// 调用
attribute vec4 a_Color    // 传了(r, g, b, a)的值
void main() {
  ...
  float brightness = luma(a_Color);
  ...
}

注意,如果调用函数时传入的参数类型与生命函数时指定的参数类型不一致,就会出错。
如:

float square(float value) {
  return value * value;
}

void main() {
  ...
  float x2 = square(10);   // 错误。应用10.0
  ...
}

因为函数声明时的参数是float类型,而调用时却传入了int类型的值。

(2)规范声明
如果函数定义在其调用之后,那么我们必须在进行调用之前先声明该函数的规范。
规范会预先告诉WebGL系统函数的参数、参数类型、返回值等等
如:

float luma(vec4, color);   // 规范声明
main() {
  ...
  float brightness = luma(a_Color);  // luma在定义之前就被调用了
  ...
}

float luma(vec4, color) {
  return 0.2126 * color.r +  0.7162 * color.g +  0.0722 * color.b;
}

(3)参数限定词
GLSL ES中,可以为参数指定限定自,以控制参数的行为。
我们可以将函数参数定义成:
(a)传递给函数的
(b)将要在函数中被复制的
(c)既是传递给函数的,也是将要在函数中被赋值的。
其中(b)和(c)都有点类似于C语言中的指针

(4)内置函数

  1. 全局变量和局部变量
    attribute、varying和uniform变量都必须声明为全局变量

(1)存储限定字
在GLSL ES中,我们经常使用attributevaryinguniform限定字来修饰变量,如下图所示。此外,有时也会使用const限定字,它表示着色器中的某个变量是恒定的常量。

(2)const变量
const变量写在类型之前,声明的同时必须对它进行初始化,声明之后就不能再去改变它们的值了。
如:

const int a = 3232

(3)Attributr变量
只能出现在顶点着色器中,只能被声明为全局变量,被用来表示逐顶点的信息。
顶点着色器中能够容纳的attribute变量的最大数目与设备有关,你可以通过访问内置的全局常量来获取最大数目的值。
但是不管设备如何,支持WebGL的环境都支持至少8个attribute变量。

(4)uniform变量
可以用在顶点着色器和片元着色器中,且必须是全局变量
uniform变量只读,可以是除了数组或结构体之外的任意类型。
如果在顶点着色器和片元着色器中声明了同名的uniform变量,那么它就会被两种着色器共享。
uniform变量包含了一致(非逐顶点/逐片元的,各顶点或各片元公用)的数据,JS应该向其传递此类数据。
比如,变换矩阵就不是逐定点的,而是所有顶点共用的,所以它在着色器中是uniform变量。

uniform mat4 u_ViewMatrix

(5)varying变量
必须是全局变量
从顶点着色器向片元着色器传输数据。
必须在两种着色器中生命同名、同类型的varying变量

varying vec2 v_TexCoord
varying vec4 v_Color

varying变量只能是以下类型:floatvec2vec3vec4mat2mat3mat4
顶点着色器中赋给varying变量的值并不是直接传给了片元着色器的varying变量,这其中发生了光栅化的过程:根据绘制的图形,对前者(顶点着色器varying变量)进行内插,然后再传递个后者(片元着色器varying变量)
正是因为varying变量需要被内插,所以我们需要限制它的数据类型

设备至少支持8个varying变量

  1. 精度限定字
    帮助着色器程序提高运行效率,削减内存开支。
    可选,不确定精度可以使用适中的默认值:
#ifdef GL_ES
precision mediump float;
#endif

WebGL中支持的3种精度

注意:
(1)在某些WebGL环境中,片元着色器可能不支持highp精度
(2)数值范围和精度实际上也是与系统环境相关,可以使用gl.getShaderPrecisionFormet()来检查

如:

mediump float size;  //  中精度浮点型变量
highp vec4 position;  //  具有高精度浮点型的vec4对象
lowp vec4 color;  //  具有低精度浮点型的vec4对象

声明着色器的默认精度,这行代码必须在顶点着色器或片元着色器的顶部:
precision 精度限定自 类型名称
表示接下来所有不以精度限定自修饰的该类型标量,其精度就是默认精度,如:

precision mediump float;
6-27.png

只有片元着色器中的float类型没有默认精度,我们需要手动指定。

  1. 预处理指令
    用来在真正编译之前对代码进行预处理,#开始
6-28.png
#ifdef GL_ES
#ifdef GL_FRAGMENT_PRECISION_HIGH
precision highp float;  //  支持高精度,限定浮点型为高精度

#else
precision mediump float;  //  不支持高精度,限定浮点型为中精度
#endif
#endif

可以只是用#version number来指定着色器使用的GLSL ES版本

可以接受的版本包括100(GLSL ES 1.00)和101(GLSL ES 1.01)。如果不使用#version命令,默认版本为100

指定版本代码:

#version 101

#version 指令必须在着色器顶部,在它之前只能有注释和空白。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容