GLSL ES 数据类型

389 阅读17分钟

零、前言

GLSL (OpenGL Shading Language) 是一种 面向过程 高级着色语言,用于图形渲染编程。

使用 GLSL 进行编写顶点着色器和片元着色器,编写两种着色器的语法上没有差异。

一、OpenGL ES 着色器语言简介

GLSL 是基于 C/C++ 基本语法和流程控制的语言,但不支持:双精度浮点数(double)、字节型(byte)、短整型(short)、长整型(long),而且取消联合体(union)、枚举(enum) 。增加了向量(vec) 、矩阵(mat)类型。

二、着色器的结构

以上篇《Hello OpenGL ES! 启航!》中三角形“顶点着色器”和“片元着色器”为例:

顶点着色器

#version 300 es
uniform mat4 uMVPMatrix;
in vec3 aPosition;
in vec4 aColor;
out vec4 vColor;
void main() {
    gl_Position = uMVPMatrix * vec4(aPosition, 1.0);
    vColor = aColor;
}

片元着色器

#version 300 es
precision mediump float;
in vec4 vColor;
out vec4 fragColor;
void main() {
    fragColor = vColor;
}

通过以上代码,可以知道一个着色器程序包括了:

  1. glsl 版本声明。 如果是 2.0 的版本,可以不用书写该注释。
  2. 全局变量声明。 这里包括输入和输出变量,着色器内部使用的全局变量等。
  3. 自定义函数。 我们需要在着色器中调用的函数,上面的着色器很简单所以就没有这一部分(后续会分享如何定义一个着色器函数)。被调用的着色器函数需要在调用函数之前,且定义的函数不可以进行递归调用。
  4. main 函数。 着色器的入口函数,每个着色器都必须定义,管线会调用该函数驱动逻辑。

二、数据类型

GLSL ES 数据类型:标量、向量、矩阵、采样器、结构体、数组、空类型。

1、标量

描述: 只具有大小,不具有方向的值。例如:质量、体积。

包括:

  • bool:布尔型,glsl 的流程控制只能使用布尔类型的值作为表达式。
bool b = true;
  • int:有符号 32 位整型。
int a = 15;     // 默认为有符号整型
  • uint:无符号 32 位整型,需要在整数后增加 "u" 或 "U" ,否则字面常量为有符号整数。
uint b = 3u;    // 无符号的类型需要使用 u 或 U 结尾

GLSL ES 整数可以使用十进制八进制(数字前用数字 0 )十六进制(数字前用 0x )

registry.khronos.org/OpenGL/spec… 4.1.3 Integers 有进行描述

// 有符号整数
int signedDec = 123;
int signedOct = 0177;
int signedHex = 0xFF;

// 无符号整数
uint unsignedDec = 123u;    
uint unsignedOct = 0177u;
uint unsignedHex = 0xFFu;
  • float:浮点型,32 位单精度,可以使用十进制和指数形式表示。
float g = 2.0; 
float j = 3e2;  

GLSL ES 没有 double 类型,所以不管是否添加 "f" 后缀,小数点形式的数字默认都被视为 float 类型,例如:1.0 和 1.0f 都是 float 类型。

虽然不加 "f" 后缀也能工作,但为了代码一致性或移植性,可以考虑添加 "f" ,添加 "f" 不会导致错误,只是在 GLSL ES 中会显得有些冗余。

2、向量

向量是由相同类型的标量组成,从上一小节知道基本类型分为:bool、int、uint、float 四种,所以向量有以下的类型。

2-1、类型

向量类型说明
vec2包含了 2 个浮点数的向量
vec3包含了 3 个浮点数的向量
vec4包含了 4 个浮点数的向量
ivec2包含了 2 个整数的向量
ivec3包含了 3 个整数的向量
ivec4包含了 4 个整数的向量
bvec2包含了 2 个布尔数的向量
bvec3包含了 3 个布尔数的向量
bvec4包含了 4 个布尔数的向量
uvec2包含了 2 个无符号整数的向量
uvec3包含了 3 个无符号整数的向量
uvec4包含了 4 个无符号整数的向量

2-2、使用方法

向量在不同场景使用时,表示着不同的含义。可以当作三种场景使用:颜色、位置、纹理坐标,具体每个分量的名称如下所示。

用法4 个分量名3 个分量名2 个分量名使用
颜色r、g、b、ar、g、br、gaColor.r 或者 aColor[0]
位置x、y、z、wx、y、zx、yaPosition.x 或者 aPosition[0]
纹理坐标s、t、p、qs、t、ps、taTexColor.s 或者 aTexColor[0]

GLSL 的向量由硬件原生支持,进行向量运算时是各分量并行一次完成(n 个分量只需要一次计算),效率很高。

2-3、使用技巧

2-3-1、向量构造函数
  1. 如果向量的构造函数内只有一个标量值,则该向量的所有分量都等于该值。
// 所有分量都为 5.0 的二维向量,等同于 vec2(5.0, 5.0)
vec2 v2 = vec2(5.0);

// 所有分量都为 5.0 的三维向量,等同于 vec3(5.0, 5.0, 5.0)
vec3 v3 = vec3(5.0);

// 所有分量都为 5.0 的四维向量,等同于 vec4(5.0, 5.0, 5.0, 5.0)
vec4 v4 = vec4(5.0);
  1. 如果向量的构造函数内有多个标量或者向量参数,向量的分量则由左向右依次被赋值。如果声明的向量维度小于构造器中向量的总维度,则会舍弃多余的分量。如果声明的向量维度大于构造器中向量的总维度,则会导致着色器编译失败。
// 正确:从左到右赋值。
vec3 v1 = vec3(1.0, 2.0, 3.0);         // v1 = (1.0, 2.0, 3.0)
vec3 v2 = vec3(vec2(1.0, 2.0), 3.0);   // v2 = (1.0, 2.0, 3.0)

// 正确:多余分量被忽略。
vec2 v3 = vec2(1.0, 2.0, 3.0);         // v3 = (1.0, 2.0),3.0被忽略
vec2 v4 = vec2(vec3(1.0, 2.0, 3.0));   // v4 = (1.0, 2.0),3.0被忽略

// 编译错误,缺少第三个分量。
vec3 v5 = vec3(1.0, 2.0);   
  1. 如果向量构造函数的参数与声明变量的类型不相符,则会将参数转换为相应的数据类型。
vec3 v = vec3(1, 2, 3.0); // 合法,会转为 (1.0, 2.0, 3.0)
2-3-2、向量赋值

以赋值表达式中的 “=” 为界,其左侧称之为 L 值,右侧称之为 R 值。

进行混合选择时,R 值可以使用一个向量的各个分量任意地组合以及重复,而 L 值则不能有任何的重复分量,但可以改变分量顺序,而且分量名称必须是同一名称组

vec4 color = vec4(0.7, 0.1, 0.5, 1.0);  // 声明一个 vec4 类型的向量 color
vec3 temp = color.agb;                  // 相当于获取到一个向量(1.0,0.1,0.5)赋值给 temp
vec4 tempL = color.aabb;                // 相当于获取到一个向量(1.0,1.0,0.5,0.5)赋值给 tempL
vec3 tempLL;                            // 声明一个 3 维向量 tempLL
tempLL.grb = color.aab;                 // 对向量 tempLL 的 3 个分量赋值

3、矩阵

3-1、类型

矩阵类型说明
mat22×2 的浮点数矩阵
mat33×3 的浮点数矩阵
mat44×4 的浮点数矩阵
mat2×22×2 的浮点数矩阵
mat2×32×3 的浮点数矩阵
mat2×42×4 的浮点数矩阵
mat3×23×2 的浮点数矩阵
mat3×33×3 的浮点数矩阵
mat3×43×4 的浮点数矩阵
mat4×24×2 的浮点数矩阵
mat4×34×3 的浮点数矩阵
mat4×44×4 的浮点数矩阵

第一个数字为列数、第二个数字为行数

3-2、矩阵访问

GLSL 中,矩阵可以看作是多个列向量的组成。

  • mat3 可以看作 3 个 vec3 向量组成。

  • 如果 matrix 为一个 mat4,可以使用 matrix[2] 取到该矩阵的第 3 列,是一个 vec4 。使用 matrix[2][2] 取得第 3 列的向量的第 3 个分量,其为一个 float 。

3-3、矩阵使用

3-3-1、构造规则
  1. 如果矩阵的构造函数内只有一个标量值,那么矩阵的对角线上的分量都等于该值,其余值为 0。

  1. 矩阵可以由多个向量构造而成

  1. 矩阵可以由大量的标量值构成,矩阵的分量由左向右依次被赋值,并且以列方向进行赋值。

  1. 初始化时矩阵 M1 的行列数 (N×N) 小于构造器中矩阵 M2 的行列数 (M×M) 时(即 N<M ),矩阵 M1 的元素值为矩阵 M2 左上角 N×N 个对应元素的值。可以结合下图理解。

  1. 初始化时矩阵 M1 的行列数 (N×M) 与构造器中矩阵 M2 的行列数 (P×Q) 不同,且 P 和 Q 之间的最大值大于 N 和 M 之间的最大值时 (假设 M1 为 mat2×4 ,M2 为 mat4×2 ) ,矩阵 M1 左上角 N×N 个元素的值为矩阵 M2 左上角 N×N 个元素的值,矩阵 M1 的其他行的元素值为 0。可以结合下图理解。

  1. 初始化时矩阵 M1 的行列数 (N×N) 大于构造器中矩阵 M2 的行列数 (M×M) 时(即 N>M ),矩阵 M1 左上角 M×M 个元素的值为矩阵 M2 的对应元素值,矩阵 M1 右下角剩余对角线元素的值为 1,矩阵 M1 剩余其他的元素值为 0。可以结合下图理解。

3-3-2、使用
float a = 6.3;
float b = 11.4;                   
float c = 12.5;           

vec3 va = vec3(2.3, 2.5, 3.8);

vec3 vb = vec3(a, b, c);            // vb = (6.3, 11.4, 12.5)

vec3 vc = vec3(vb.x, va.y, 14.4);   // vc = (6.3, 2.5, 14.4)

mat3 ma = mat3(1.0, 2.0, 3.0,       // ma = (1.0, 4.0, 7.0, 
               4.0, 5.0, 6.0,       //       2.0, 5.0, 8.0, 
               7.0, 8.0, c);        //       3.0, 6.0, 12.5)
               
mat3 mb = mat3(va, vb, vc);         // mb = (2.3, 6.3, 6.3,
                                    //       2.5, 11.4, 2.5,
                                    //       3.8, 12.5, 14.4)
                                    
mat3 mc = mat3(va, vb, 1.0, 2.0, 3.0);  // mc = (2.3, 6.3, 1.0,
                                        //       2.5, 11.4, 2.0,
                                        //       3.8, 12.5, 3.0)
                                        
mat3 md = mat3(2.0) ;                   // md = (2.0, 0.0, 0.0,
                                        //       0.0, 2.0, 0.0,   
                                        //       0.0, 0.0, 2.0)
                                        
mat4x4 me = mat4x4(3.0);                // me = (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)
                                        
mat3x3 mf = mat3x3(me);         // mf = (3.0,0.0,0.0,
                                //       0.0,3.0,0.0,
                                //       0.0,0.0,3.0)
                                
vec2 vd = vec2(a, b);           // vd = (6.3, 11.4)

mat4x2 mg = mat4x2(vd, vd, vd, vd); // mg = (6.3, 6.3, 6.3, 6.3,
                                    //       11.4, 11.4, 11.4, 11.4)
                                    
mat2x3 mh = mat2x3(mg);             // mh = (6.3, 6.3,
                                    //       11.4,11.4,  
                                    //       0.0, 0.0)
                                    
mat4x4 mj = mat4x4(mf);             // mj = (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)

4、采样器

描述: 用于纹理采样,一个采样器变量代表一幅纹理或是一套纹理贴图。

采样器类型描述
sampler2D用于访问浮点型的二维纹理
sampler3D用于访问浮点型的三维纹理
samplerCube用于访问浮点型的立方贴图纹理
samplerCubeShadow用于访问浮点型的立方阴影纹理
sampler2DShadow用于访问浮点型的二维阴影纹理
sampler2DArray用于访问浮点型的 2D 纹理数组
sampler2DArrayShadow用于访问浮点型的 2D 阴影纹理数组
isampler2D用于访问整型的二维纹理
isampler3D用于访问整型的三维纹理
isamplerCube用于访问整型的立方贴图纹理
isampler2DArray用于访问整型的 2D 纹理数组
usampler2D用于访问无符号整型的二维纹理
usampler3D用于访问无符号整型的三维纹理
usamplerCube用于访问无符号整型的立方贴图纹理
usampler2DArray用于访问无符号整型的 2D 纹理数组

值得注意:

  • 采样器变量不能在着色器中进行初始化,只能通过 OpenGL API 函数在 CPU 端绑定纹理。
  • 采样器变量用 uniform 限定符修饰,从宿主语言(如 C/C++、Java、Kotlin 等)接收传递进着色器的值。
  • 采样器变量可以用作函数的参数,但是作为函数参数时不可以使用 outinout 修饰符来修饰。

5、结构体

5-1、描述

和 C/C++ 语言类似,用 struct 进行定义,使用方式和 C/C++ 一样。

5-2、用法

struct info{            
    vec3 color;         
    vec3 position;      
    vec2 textureCoor;  
};

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

5-3、构造函数

构造函数内的每一个值都会按顺序赋给结构体内相应的成员,要求每一个值的类型都要与结构体内的成员类型相匹配。

struct light{           // 定义结构体 light
    float intensity;    // 声明 float 型成员
    vec3 position;      // 声明 vec3 型成员
};

light lightVar = light(2.0, vec3(1.0, 2.0, 3.0)); // 创建 light 结构体的实例

6、数组

6-1、描述

和大多数语言一样,GLSL 也支持数组类型,使用的规则也类似。

6-2、构造数组

// 第一种,包含 20 个 vec3 的数组
vec3 position[20];

// 第二种,定义同时指定内容
float x[] = float[2](1.0, 2.0);
float y[] = float[](1.0, 1.0, 1.0);

值得注意:

OpenGL ES 3.0 的 GLSL 只支持一维数组。

7、空类型

描述: 仅仅用于声明不返回任何值函数的类型。

void main(){}

三、变量声明、作用域

因为 GLSL 是基于 C/C++ 语言语法,所以很多语法上有相同的地方,在变量声明、作用域与 C/C++ 语言类似。

  • 变量可以在着色器中任何地方进行声明。
  • 变量因作用域可以分为局部变量全局变量
vec4 lightPosition = vec4(1.0);     // 声明了全局变量 lightPosition ,作用域为整个着色器。

void fun() {
    int count = 4;                  // 声明了局部变量 count,作用域为函数 `fun` 
}

四、变量的特殊规则

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

in float angleSpan;     // 不能对 “全局输入变量” 进行初始化
uniform int count;      // 不能对 “一致变量” 进行初始化
out vec3 position;      // 不能对 “全局的输出变量” 进行初始化

着色器中应少使用字面常量,多用常量代替字面量,可以提高代码可维护性和性能,避免重复计算,例如下面代码。

// 使用常量代替字面常量
const float PI = 3.14159;
const vec3 LIGHT_COLOR = vec3(1.0, 1.0, 1.0);

void main() {
  // 使用常量进行计算
  float radius = 2.0 * PI;
  vec3 finalColor = vec3(0.5) * LIGHT_COLOR;
}

五、运算符的特殊表现

  1. 向量以及矩阵中使用 ++-- ,则向量或矩阵的每个元素都加 1 或者减 1。
vec3 v = vec3(1.0, 2.0, 3.0);
++v;    // v = (2.0, 3.0, 4.0)
v--;    // v = (1.0, 2.0, 3.0)

mat2 m = mat2(1.0, 2.0, 3.0, 4.0);
m++;    // m = [2.0, 3.0, 4.0, 5.0]
  1. 向量和矩阵相乘,矩阵和矩阵相乘则遵循线性代数的乘法规则。
mat3 m = mat3 (         // m = [ 1.0, 4.0, 7.0,
    1.0, 2.0, 3.0,      //       2.0, 5.0, 8.0,
    4.0, 5.0, 6.0,      //       3.0, 6.0, 9.0 ]
    7.0, 8.0, 9.0  
);
vec3 v = vec3(10.0, 20.0, 30.0);    // v = [ 10.0,
                                    //       20.0,
                                    //       30.0 ]

vec3 result = m * v;        // result = (1.0*10.0 + 4.0*20.0 + 7.0*30.0,
                            //           2.0*10.0 + 5.0*20.0 + 8.0*30.0,
                            //           3.0*10.0 + 6.0*20.0 + 9.0*30.0)
                            // result = (300.0, 360.0, 420.0)
  1. 两个向量进行 “乘除加减” 运算时,按照两个向量的分量进行 “乘除加减”,最终得到一个新向量。
vec3 a = vec3(1.0, 2.0, 3.0);
vec3 b = vec3(4.0, 5.0, 6.0);

vec3 sum = a + b;      // sum = (5.0, 7.0, 9.0)
vec3 diff = a - b;     // diff = (-3.0, -3.0, -3.0)
vec3 product = a * b;  // product = (4.0, 10.0, 18.0)
vec3 division = a / b; // division = (0.25, 0.4, 0.5)
  1. 关系运算符(<、>、<= 、>=)只能用在浮点数或整数标量的操作中,通过关系运算符的运算将产生一个布尔型的值。
float a = 3.0;
float b = 5.0;
bool result1 = a < b;   // true
bool result2 = a >= b;  // false

// 错误用法示例:
// vec2 v1 = vec2(1.0, 2.0);
// vec2 v2 = vec2(2.0, 1.0);
// bool res = v1 < v2;  // 编译错误! 不能直接比较向量
  1. 比较两个向量中的每一个分量的大小结果,需要使用内置函数 lessThanlessThanEqualgreaterThangreaterThanEqual,这些函数会返回一个向量,向量中则为比较结果。
vec3 v1 = vec3(1.0, 5.0, 3.0);
vec3 v2 = vec3(2.0, 3.0, 3.0);

bvec3 less = lessThan(v1, v2);                  // (true, false, false)
bvec3 greater = greaterThan(v1, v2);            // (false, true, false)
bvec3 lessEqual = lessThanEqual(v1, v2);        // (true, false, true)
bvec3 greaterEqual = greaterThanEqual(v1, v2);  // (false, true, true)
  1. 等于运算符(==、!=)可以用在任何类型数据的操作中,操作将对左右两个操作数值的每一个分量分别进行比较,得出一个布尔型的值,说明左右两个操作数是否完全相等。
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, 4.0);

bool isEqual1 = (v1 == v2);  // true,所有分量都相等
bool isEqual2 = (v1 == v3);  // false,至少有一个分量不相等
bool notEqual = (v1 != v3);  // true,至少有一个分量不相等
  1. 想要得到两个向量中的每一个元素是否相等的结果,则可以调用内置函数 equalnotEqual
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, 4.0);

// 分量级比较
bvec3 eq = equal(v1, v3);       // (true, true, false)
bvec3 neq = notEqual(v1, v3);   // (false, false, true)
  1. 逻辑运算符包括 “与(&&)”、“或(||)”、“非(!)”、“异或(^^)” 4 种操作类型,只可以用在类型为布尔标量的表达式中,不可以用在矩阵中
bool a = true;
bool b = false;

bool andResult = a && b;    // false
bool orResult = a || b;     // true
bool notResult = !a;        // false
bool xorResult = a ^^ b;    // true (仅当一个为 true 另一个为 false 时结果为 true)

// 错误用法示例:
// vec2 bv1 = vec2(true, false);
// vec2 bv2 = vec2(false, true);
// vec2 andVec = bv1 && bv2;  // 编译错误! 不能用于向量
  1. 位运算符包括取 “反(~)”、“左移(<<)”、“右移(>>)”、“与(&)”、“或(|)”、“异或(^)” 六种操作类型,这些操作符只适用于 有符号或者无符号的整型标量 或者 整型向量 的类型。其中 “与”、“或”、“异或” 要求左右两边的操作数必须是相同长度的整型量。
// 整型标量的位运算
int a = 5;   // 二进制:101
int b = 3;   // 二进制:011
int bitwiseNot = ~a;         // 二进制:...11111010
int leftShift = a << 1;      // 二进制:1010,值为10
int rightShift = a >> 1;     // 二进制:10,值为2
int bitwiseAnd = a & b;      // 二进制:001,值为1
int bitwiseOr = a | b;       // 二进制:111,值为7
int bitwiseXor = a ^ b;      // 二进制:110,值为6

// 整型向量的位运算
ivec3 v1 = ivec3(5, 7, 9);
ivec3 v2 = ivec3(3, 4, 1);
ivec3 vAnd = v1 & v2;                       // vAnd = (1, 4, 1)
ivec3 vOr = v1 | v2;                        // vOr = (7, 7, 9)
ivec3 vXor = v1 ^ v2;                       // vXor = (6, 3, 8)
ivec3 vNot = ~v1;                           // 每个分量按位取反
ivec3 vLeftShift = v1 << ivec3(1, 2, 3);    // vLeftShift = (10, 28, 72)

六、类型转换

GLSL 的赋值没有自动类型转换或提升功能,对类型的赋值要完全一致。 函数的调用,形参和实参的类型也需要完全一样。

float a = 1;    // 是错误的,因为左侧的 a 是浮点型,右侧的 1 是整型
                // cannot convert from 'const int' to 'float'

float a = 1.0;  // 正确

进行转换类型,只能通过相应类型的构造函数进行。

float f = 1.0;       
bool b = bool(f);     // 将浮点数转换成布尔类型,将非 0 的数字转为 true,0 转为 false
float f1 = float(b);  // 将布尔值转变为浮点数,true 转换为 1.0,false 转换为 0.0
int c = int(f1);      // 将浮点数转换成有符号或者无符号整型,直接去掉小数部分

不提供类型转换是因为可以避免某些类型转换带来的性能、复杂性缺陷,简化语言对应的硬件实现。

七、浮点数精度问题

顶点着色器有默认 highp 精度,所以可以不用指定精度,直接使用浮点数即可。

片元着色器没有默认浮点数精度,所以需要指定浮点数精度,否则可能导致编译错误。

1、浮点精度类型

精度类型等级
lowp低精度,至少 8 位尾数
mediump中精度,至少 10 位尾数
highp高精度,至少 16 位尾数

大多数情况,片元着色器使用中精度 mediump 即可满足。

值得注意的是,浮点数的精度指定不仅仅只是针对标量类型中的 float 类型,而是包括了浮点类型的向量、浮点类型的矩阵和浮点类型相关等类型。

2、使用方式

在顶点着色器和片元着色器中,精度的指定有以下两种

2-1、每个属性独自设置

如果需要不同变量不同的精度,则可以考虑这种方式设置

lowp float color;       // color 为 float 型变量,精度为 lowp
in mediump vec2 coord;  // coord 为 vec2 型变量,精度为 mediump
highp mat4 m;           // m 为 mat4 型变量,精度为 highp

2-2、统一添加

如果一个片元着色器中,希望所有的 float 变量精度是一样的,可以在片元着色器的第一句编写以下代码,则作用整一个片元着色器。

// 格式为 “precision <精度> <类型>”
precision mediump float

这种格式在很多片元着色器中经常看到。

值得注意,如果链接到一起的两个着色器中相同的 uniform 变量,则必须是相同的精度限定符。

2-3、精度不只是为 float 类型设定

精度同样可以作用于 int 、采样器类型,只是没有像 float 类型一样强制显示指定,这是因为浮点数的精度对于图形渲染和计算非常重要。

八、写在最后

EglBox-Android 项目地址:github.com/zincPower/E… (如果对你有所帮助或喜欢的话,赏个 star 吧,码字不易,请多多支持)

如果觉得本篇博文对你有所启发或是解决了困惑,点个赞或关注我呀

公众号搜索 “江澎涌”,更多优质文章会第一时间分享与你。