<三>着色器编程语言GLSL

1.基本数据类型

1.0 标量:

  • bool 布尔类型 true,false
  • int 带符号的整数 signed integer
  • float 带符号的浮点数 floating scalar
    需要注意的是float,在顶点着色器中可以直接声明使用浮点类型变量,而片元着色器中需要指定浮点类型变量的精度,否则会产生编译错误。

glsl提供3种精度类型:

  • lowp
  • mediump
  • highp
    声明精度有2种方法:
  • 一:
    ex: lowp float color;
    ex: varying mediump vec2 coord;
    ex: highp mat4 m;
  • 二:在片元着色器第一句声明,整个片元着色器将使用同一精度
    precision mediump float;

如果没有显式表明精度的变量,都会按照设定好的默认精度来处理.

1.1 向量:

image
image
glsl中的向量(vec2,vec3,vec4)往往有特殊的含义,比如可能代表了一个空间坐标(x,y,z,w),或者代表了一个颜色(r,g,b,a),再或者代表一个纹理坐标(s,t,p,q)
所以glsl提供了一些更人性化的分量访问方式.

  • 将一个向量看做颜色时,可以使用r, g, b, a这4个分量名,vector.rgba 其中rgba 可以任意组合

  • 将一个向量看做位置时,可以使用x, y, z, w这4个分量名,vector.xyzw 其中xyzw 可以任意组合

  • 将一个向量看做纹理坐标时,可以使用s, t, p, q这4个分量名,vector.stpq 其中stpq 可以任意组合

  • 向量还可以看成一个数组,通过下标的方式来访问
    ex:

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)

vec4 xyzw = v.xyzw; //vec4(1.0,2.0,3.0,1.0);
vec4 rgba = v.rgba; //vec4(1.0,2.0,3.0,1.0);

1.2 矩阵

image
3D场景中的移位,旋转,缩放等变换都是由矩阵的运算来实现的
矩阵属于比较专业的数学知识,下一章会单独列出矩阵等相关的数学专业术语基础认识。这里只需要知道在3D场景中的移动、旋转、缩放等变换都是通过矩阵运算实现的就可以了。

1.3 采样器

image
这里需要注意一点,采样器变量是输入变量,不能在着色器中初始化。一般情况下采样器变量都是用uniform限定符来修饰,从宿主语言传递进着色器的值。

1.4 空类型

void 空类型,不返回任何值

以上是glsl的基本数据类型,需要注意的是:

  • glsl内建变量都是以“gl_”开头,因此用户自定义的变量不允许使用“gl_”开头。
  • 属性变量,一致变量和易变变量在声明的时候一定不能进行初始化。

2.基本结构和数组

  • 结构体:
struct type-name{

} 

类似c语言中的 结构体
例如:

//定义结构体CubeInfo
struct CubeInfo{
    vec3 color;
    vec3 position;
    vec2 textureCoor;
}

//声明一个CubeInfo类型的变量
CubeInfo info;
  • 数组
    glsl只支持1维数组,数组可以是结构体的成员
    例如:
float foo[3] 

3.运算符

image
备注:左值与右值

  • 左值:表示一个储存位置,可以是变量,也可以是表达式,但表达式最后的结果必须是一个储存位置.

  • 右值:表示一个值, 可以是一个变量或者表达式再或者纯粹的值.

  • 操作符的优先级:决定含有多个操作符的表达式的求值顺序,每个操作的优先级不同.

  • 操作符的结合性:决定相同优先级的操作符是从左到右计算,还是从右到左计算。

4.类型转换

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;

如果运算中遇到需要类型转换的情况,必须要显式转换,下面列几个例子

ex1: float 与 int:

float与float , int与int之间是可以直接运算的,但float与int不行.它们需要进行一次显示转换.即要么把float转成int: int(1.0)
,要么把int转成float: float(1) ,以下表达式都是正确的:

int a=int(2.0);
float a= float(2);

int a=int(2.0)*2 + 1;
float a= float(2)*6.0+2.3;

ex2: float 与 vec(向量) mat(矩阵):

vec,mat这些类型其实是由float复合而成的,当它们与float运算时,其实就是在每一个分量上分别与float进行运算,这就是所谓的逐分量运算
glsl里大部分涉及vec,mat的运算都是逐分量运算,不过也有特殊的例子后面会提到。
逐分量运算是线性的,这就是说 vec 与 float 的运算结果是还是 vec.
int 与 vec,mat之间是不可运算的, 因为vec和mat中的每一个分量都是 float 类型的. 无法与int进行逐分量计算.

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)

ex3: 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)

ex4: 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.)

向量与矩阵的乘法规则如下:
image

ex5. 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);

矩阵乘法规则如下:
image

5.变量限定符

image

5.1 const:

被const限定符修饰的变量初始化后不可修改,和C的规则类似。
除了局部变量,函数参数也可以使用const修饰符.
需要注意的是,结构变量可以用const修饰,但结构中的字段不行.

struct light {
        vec4 color;
        vec3 pos;
        //const vec3 pos1; //结构中的字段不可用const修饰会报错.
    };
const light lgt = light(vec4(1.0), vec3(0.0)); //结构变量可以用const修饰

const变量必须在声明时就初始化,例如:

const vec3 v3 = vec3(0.,0.,0.)

另:如果局部变量、函数参数能且只能使用const限定符

5.2 attribute:

attribute变量是全局且只读的.
它只能在vertex shader中使用,只能与浮点数,向量或矩阵变量组合
一般attribute变量用来放置程序传递来的模型顶点,法线,颜色,纹理等数据它可以访问数据缓冲区

attribute vec4 a_Position;

5.2 uniform:

uniform变量是全局且只读的
在整个shader执行完毕前其值不会改变,他可以和任意基本类型变量组合,
一般我们使用uniform变量来放置外部程序传递来的环境数据(如点光源位置,模型的变换矩阵等等)
这些数据在运行中显然是不需要被改变的.

5.2 varying:

varying类型变量是 vertex shader 与 fragment shader 之间的信使,一般我们在 vertex shader 中修改它然后在fragment shader使用它,但不能在fragment shader中修改它.

//顶点着色器
varying vec4 v_Color;
void main(){ 
    ...
    v_Color = vec4(1.,1.,1.,1);
}

//片元着色器
...
varying vec4 v_Color;
void main() {
  gl_FragColor = v_Color;
}
...

要注意全局变量限制符只能为 const、attribute、uniform和varying中的一个.不可复合使用.

6.函数参数限定符:

函数的参数默认是以拷贝的形式传递的,也就是值传递,任何传递给函数参数的变量,其值都会被复制一份,然后再交给函数内部进行处理.
我们可以为参数添加限定符来达到传递引用的目的。
glsl中提供的参数限定符如下:
image

  • in
    in是函数参数的默认限定符,最终真正传入函数形参的其实是实参的一份拷贝.在函数中,修改in修饰的形参不会影响到实参变量本身.
  • out
    它的作用是向函数外部传递新值,out模式下传递进来的参数是write-only的(可写不可读).就像是一个"坑位",坑位中的值需要函数给他赋予.
    在函数中,修改out修饰的形参会影响到实参本身.
  • inout
    inout下,形参可以被理解为是一个带值的"坑位",及可读也可写,在函数中,修改inout修饰的形参会影响到实参本身.

7.函数

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;
}

8.构造函数

glsl中变量可以在声明的时候初始化
如:
float pSize = 10.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);

9.类型转换

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);

10.invariant关键字:

由于shader在编译时会进行一些内部优化,可能会导致同样的运算在不同shader里结果不一定精确相等.这会引起一些问题,尤其是vertx shader向fragmeng shader传值的时候.
所以,需要使用invariant 关键字来显式要求计算结果必须精确一致.

invariant varying texCoord; //varying在传递数据的时候声明为invariant

也可以使用 #pragma STDGL invariant(all)来命令所有输出变量必须精确一致,但这样会限制编译器优化程度,降低性能。

#pragma STDGL invariant(all) //所有输出变量为 invariant

11.预编译指令:

在GLSL中,以 # 开头的是预编译指令,常用的有:

#define 
#undef 
#if 
#ifdef 
#ifndef 
#else
#elif 
#endif 
#error 
#pragma 
#extension 
#version #line

比如 #version 100 他的意思是规定当前shader使用 GLSL ES 1.00标准进行编译,如果使用这条预编译指令,则他必须出现在程序的最开始位置.

12.内置的宏:

LINE : 当前源码中的行号.
VERSION : 一个整数,指示当前的glsl版本 比如 100 ps: 100 = v1.00
GL_ES : 如果当前是在 OPGL ES 环境中运行则 GL_ES 被设置成1,一般用来检查当前环境是不是 OPENGL ES.
GL_FRAGMENT_PRECISION_HIGH : 如果当前系统glsl的片元着色器支持高浮点精度,则设置为1.一般用于检查着色器精度.

  • ex1:
    通过判断系统环境,来选择合适的精度:
#ifdef GL_ES //
#ifdef GL_FRAGMENT_PRECISION_HIGH
precision highp float;
#else
precision mediump float;
#endif
#endif

  • ex2
    自定义宏:
#define NUM 100
#if NUM==100
#endif

13.内置的特殊变量

glsl程序使用一些特殊的内置变量与硬件进行沟通.
他们大致分成两种 一种是 input类型,他负责向硬件(渲染管线)发送数据.
另一种是output类型,负责向程序回传数据,以便编程时需要.

  • 在 vertex Shader 中:
    output 类型的内置变量:
    image

  • 在 fragment Shader 中:
    input 类型的内置变量:
    image

  • output 类型的内置变量:
    image

14.内置的常量

glsl提供了一些内置的常量,用来说明当前系统的一些特性. 有时我们需要针对这些特性,对shader程序进行优化,让程序兼容度更好.

  • 在 vertex Shader 中:
  1. const mediump int gl_MaxVertexAttribs>=8
    gl_MaxVertexAttribs 表示在vertex shader(顶点着色器)中可用的最大attributes数.
    这个值的大小取决于 OpenGL ES 在某设备上的具体实现,不过最低不能小于 8 个.
  2. const mediump int gl_MaxVertexUniformVectors >= 128
    gl_MaxVertexUniformVectors 表示在vertex shader(顶点着色器)中可用的最大uniform vectors数.
    这个值的大小取决于 OpenGL ES 在某设备上的具体实现,不过最低不能小于 128 个.
  3. const mediump int gl_MaxVaryingVectors >= 8
    gl_MaxVaryingVectors 表示在vertex shader(顶点着色器)中可用的最大varying vectors数.
    这个值的大小取决于 OpenGL ES 在某设备上的具体实现,不过最低不能小于 8 个.
  4. const mediump int gl_MaxVertexTextureImageUnits >= 0
    gl_MaxVaryingVectors 表示在vertex shader(顶点着色器)中可用的最大纹理单元数(贴图).
    这个值的大小取决于 OpenGL ES 在某设备上的具体实现,甚至可以一个都没有(无法获取顶点纹理)
  5. const mediump int gl_MaxCombinedTextureImageUnits >= 8
    gl_MaxVaryingVectors 表示在 vertex Shader和fragment Shader总共最多支持多少个纹理单元.
    这个值的大小取决于 OpenGL ES 在某设备上的具体实现,不过最低不能小于 8 个.
  • 在 fragment Shader 中:
  1. const mediump int gl_MaxTextureImageUnits >= 8
    gl_MaxVaryingVectors 表示在 fragment Shader(片元着色器)中能访问的最大纹理单元数,
    这个值的大小取决于 OpenGL ES 在某设备上的具体实现,
    不过最低不能小于 8 个.

  2. const mediump int gl_MaxFragmentUniformVectors >= 16

gl_MaxFragmentUniformVectors 表示在 fragment Shader(片元着色器)中可用的最大uniform vectors数
这个值的大小取决于 OpenGL ES 在某设备上的具体实现,不过最低不能小于 16 个.

  1. const mediump int gl_MaxDrawBuffers = 1

gl_MaxDrawBuffers 表示可用的drawBuffers数
在OpenGL ES 2.0中这个值为1, 在将来的版本可能会有所变化.

另:glsl中还有一种内置的uniform状态变量, gl_DepthRange 它用来表明全局深度范围.

struct gl_DepthRangeParameters {
 highp float near; // n
 highp float far; // f
 highp float diff; // f - n
 };
 uniform gl_DepthRangeParameters gl_DepthRange;

除了 gl_DepthRange 外的所有uniform状态常量都已在glsl 1.30 中废弃.

15.流控制

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;

16.内置函数库

glsl提供了非常丰富的函数库,按功能可以分为7大类

16.1 通用函数:

类型 T可以是 float, vec2, vec3, vec4,且可以逐分量操作.
image

16.2 角度&三角函数:

类型 T可以是 float, vec2, vec3, vec4,且可以逐分量操作.
image

16.3 指数函数:

类型 T可以是 float, vec2, vec3, vec4,且可以逐分量操作.
image

16.4 几何函数:

类型 T可以是 float, vec2, vec3, vec4,且可以逐分量操作.
image

16.5 矩阵函数:

mat可以为任意类型矩阵.
image

16.6 向量函数:

类型 T可以是 vec2, vec3, vec4, 且可以逐分量操作.
image

bvec指的是由bool类型组成的一个向量:

vec3 v3= vec3(0.,0.,0.);
vec3 v3_1= vec3(1.,1.,1.);
bvec3 aa= lessThan(v3,v3_1); //bvec3(true,true,true)

16.7 纹理查询函数:

图像纹理有两种 一种是平面2d纹理,另一种是盒纹理,针对不同的纹理类型有不同访问方法.
纹理查询的最终目的是从sampler中提取指定坐标的颜色信息.
函数中带有Cube字样的是指 需要传入盒状纹理.
带有Proj字样的是指带投影的版本.

  • 以下函数只在vertex shader中可用:
vec4 texture2DLod(sampler2D sampler, vec2 coord, float lod);
vec4 texture2DProjLod(sampler2D sampler, vec3 coord, float lod);
vec4 texture2DProjLod(sampler2D sampler, vec4 coord, float lod);
vec4 textureCubeLod(samplerCube sampler, vec3 coord, float lod);
  • 以下函数只在fragment shader中可用:
vec4 texture2D(sampler2D sampler, vec2 coord, float bias);
vec4 texture2DProj(sampler2D sampler, vec3 coord, float bias);
vec4 texture2DProj(sampler2D sampler, vec4 coord, float bias);
vec4 textureCube(samplerCube sampler, vec3 coord, float bias);
  • 以下函数在 vertex shader 与 fragment shader 中都可用:
vec4 texture2D(sampler2D sampler, vec2 coord);
vec4 texture2DProj(sampler2D sampler, vec3 coord);
vec4 texture2DProj(sampler2D sampler, vec4 coord);
vec4 textureCube(samplerCube sampler, vec3 coord);

Shader案例:

Vertex Shader:

uniform mat4 mvp_matrix; //透视矩阵 * 视图矩阵 * 模型变换矩阵
uniform mat3 normal_matrix; //法线变换矩阵(用于物体变换后法线跟着变换)
uniform vec3 ec_light_dir; //光照方向
attribute vec4 a_vertex; // 顶点坐标
attribute vec3 a_normal; //顶点法线
attribute vec2 a_texcoord; //纹理坐标
varying float v_diffuse; //法线与入射光的夹角
varying vec2 v_texcoord; //2d纹理坐标
void main(void)
{
 //归一化法线
 vec3 ec_normal = normalize(normal_matrix * a_normal);
 //v_diffuse 是法线与光照的夹角.根据向量点乘法则,当两向量长度为1是 乘积即cosθ值
 v_diffuse = max(dot(ec_light_dir, ec_normal), 0.0);
 v_texcoord = a_texcoord;
 gl_Position = mvp_matrix * a_vertex;
}

Fragment Shader:

precision mediump float;
uniform sampler2D t_reflectance;
uniform vec4 i_ambient;
varying float v_diffuse;
varying vec2 v_texcoord;
void main (void)
{
 vec4 color = texture2D(t_reflectance, v_texcoord);
 //这里分解开来是 color*vec3(1,1,1)*v_diffuse + color*i_ambient
 //色*光*夹角cos + 色*环境光
 gl_FragColor = color*(vec4(v_diffuse) + i_ambient);
}
posted @ 2024-12-27 19:08  EricShx  阅读(211)  评论(0)    收藏  举报