前言
本篇将继续学习WebGL的内容
GLSL语法测试
在GLSL ES中,我们是没法像js中一样,调试的时候,只需要console.log
就可以把我们的数据给打印出来。
GLSL ES数据的输出
我现在片元着色器里写了一个4维向量的加法运算,我想把它打印出来看看v 的结果是不是如我想的那样。
1 | <script id="fragmentShader" type="x-shader/x-fragment"> |
我们知道,这个向量相加的结果应该是一个新的向量(1+5,2+6,3+7,4+8),答案为(6,8,10,12)
这里我们可以通过先把GLSL ES 数据画到画布中,然后再从画布的像素数据中解析出GLSL ES 数据的方式实现一个类似console.log
的效果
1 | <script id="fragmentShader" type="x-shader/x-fragment"> |
因为gl_FragColor 中1个单位的分量就相当于canvas画布中255的分量值,所以我让上面的向量v直接除以255。
注:此方法只适用于向量因子在0-255的数据。我们在此先不考虑太深,先把语法跑通。
我们可以先在画布中画一个点:
1 | <script id="vertexShader" type="x-shader/x-vertex"> |
效果如下:
接下来我们就可以获取canvas 画布中的像素数据了。
在canvas 画布中获取像素数据
在此先给会canvas 2d 的同学提一下,在我们通过canvas.getContext() 方法获取2d或webgl 上下文对象的同时,也决定了canvas画布的命运。
canvas.getContext(‘2d’) 方法会让canvas画布变成2d画布,2d的canvas画布可以通过ctx.getImageData() 方法获取画布中的像素;
canvas.getContext(‘webgl’) 方法会让canvas画布变成webgl画布,webgl画布需要通过ctx.readPixels() 方法获取画布中的像素。
1.建立一个8位无符号型数组,用于存储一个像素的数据。
1 | const pixel = new Uint8Array(4); |
2.从画布中采集一个像素出来。
1 | gl.readPixels( |
gl.readPixels(x, y, width, height, format, type, pixels)
- x, y:从哪里采集像素
- width, height:采集多大一块区域的像素
- format:数据格式
- type:数据类型
- gl.UNSIGNED_BYTE
- gl.UNSIGNED_SHORT_5_6_5
- gl.UNSIGNED_SHORT_4_4_4_4
- gl.UNSIGNED_SHORT_5_5_5_1
- gl.FLOAT
- pixels:装像素的容器
- Uint8Array 对应 gl.UNSIGNED_BYTE
- Uint16Array 对应 gl.UNSIGNED_SHORT_5_6_5, gl.UNSIGNED_SHORT_4_4_4_4, 或者 gl.UNSIGNED_SHORT_5_5_5_1
- Float32Array 对应 gl.FLOAT
3.打印pixel
1 | console.log(pixel); //Uint8Array(4) [6, 8, 10, 12] |
打印多个向量
1.用不同的向量数据画圆环。
1 | <!-- 顶点着色器 --> |
如图所示
2.按照比例关系从四个圆环中取色
1 | const vw = 512 / 8; |
上面这种方式,在特定的一些高分辨率设备上,会出现兼容性问题,画出来的圆会比较小,取样会失败
打印多个向量-适配不同分辨率的设备
我接下来会基于片元位置和画布的宽高比绘图,然后再基于这样的比例取样。
1.绘制一个充满画布的矩形面
1 | const source = new Float32Array([ |
1 | <!-- 顶点着色器 --> |
2.用js取上面四个格子的中点
1 | const [w, h] = [2, 2] |
GLSL ES 概述和基本规范
GLSL ES是在GLSL(OpenGL着色器语言)的基础上,删除和简化了一部分功能后形成的,ES版本主要降低了硬件功耗,减少了性能开销。
实际上WebGL并不支持GLSL ES的所有特性,所以大家以后在WebGL API里可能会遇到部分参数无效的情况。
GLSL ES 是写在着色器中的,其具备以下基本规范:
- 大小写敏感
- 语句末尾必须要有分号
- 以main函数为主函数
- 注释语法和js 一样
单行: //
多行: /**/ - 基本数据类型:
数字型
浮点型 float,如1.0
整型 int,如1
布尔型 bool
true
false
变量
声明变量的方法
GLSL ES是强类型语言,在声明变量的时候应该指明变量类型,如:
1 | float f=1.0; |
变量命名规范
- 只能包括a-z,A-Z,0-9,_
- 变量名首字母不能是数字
- 不能是GLSL 关键字,如attribute,vec4,bool
- 不能是GLSL 保留字,如cast,class,long
- 不能以下单词开头:gl_, webgl_, webgl
变量的赋值
变量使用等号=赋值,=两侧的数据类型需要一致。
1 | int i=8; // ✔ |
变量的类型转换
有时候,我们需要将类型a 的数据转成类型b 的数据,交给类型为b 的变量。
比如,我要把整形数字转成浮点型交给浮点型变量:
1 | float f=float(8) |
上面的float() 方法便是类型转换方法。
基础数据的转换有以下几种:
- 浮点转整数:int(float)
- 布尔转整数:int(bool)
- 整数转浮点:float(int)
- 布尔转浮点:float(bool)
- 整数转布尔:bool(int)
- 浮点转布尔:bool(float)
向量
向量类型
GLSL ES 支持2、3、4维向量,根据分量的数据类型,向量可以分为3类:
vec2、vec3、vec4:分量是浮点数
ivec2、ivec3、ivec4:分量是整数
bvec2、bvec3、bvec4:分量是布尔值
向量的创建
在GLSL ES 中,向量占有很重要的地位,所以GLSL ES为其提供了非常灵活的创建方式。
比如:
1 | vec3 v3 = vec3(1.0, 0.0, 0.5); // (1.0, 0.0, 0.5) |
我们还可以将多个向量合在一起:
1 | vec4 v4b=vec4(v2,v4); // (1.0,0.0,1.0,1.0) |
注:= 两侧的数据类型必须一致,比如下面的写法会报错:
1 | vec4 v4 = vec2(1.0); //错的 |
向量分量的访问
向量分量的访问方式:
1.通过分量属性访问
1 | v4.x, v4.y, v4.z, v4.w // 齐次坐标 |
2.将分量的多个属性连在一起,可以获取多个向量
1 | vec4 v4 = vec4(1.0,2.0,3.0,4.0); |
3.通过分量索引访问
1 | v4[0], v4[1], v4[2], v4[3] |
用上面的方法访问到向量后,也可以用=号为向量赋值。
1 | v4.x=1.0 |
矩阵
矩阵的类型
GLSL ES 支持2、3、4维矩阵:
- mat2
- mat3
- mat4
矩阵中的元素都是浮点型。
矩阵的建立
GLSL ES 中的矩阵是列主序的,在建立矩阵的时候,其参数结构有很多种。
1.浮点数,其参数是按照列主序排列的。
1 | mat4 m=mat4( |
2.向量
1 | vec4 v4_1=vec4(1,2,3,4); |
3.浮点+向量
1 | vec4 v4_1=vec4(1,5,9,13); |
4.单个浮点数
1 | mat4 m=mat4(1); |
注:如矩阵中的参数数量大于1,小于矩阵元素数量,会报错
1 | mat4 m4 = mat4(1.0,2.0); |
矩阵的访问
1.使用[] 可以访问矩阵的某一行。
1 | mat4 m=mat4( |
2.使用 m[y][x] 方法,可以访问矩阵第y行,第x列的元素。
1 | mat4 m=mat4( |
3.m[y] 可以理解为一个向量,其内部的元素,可以像访问向量元素一样去访问。
1 | mat4 m=mat4( |
注:我们在此要注意一下[] 中索引值的限制。
[] 中的索引值只能通过以下方式定义:
1.整形字面量,如0,1,2,3
1 | m[0] |
2.用const 修饰的变量
1 | const int y=0; |
注:以下写法是错误的
1 | int y=0; |
3.循环索引
1 | mat4 n=mat4( |
前面三项组成的表达式
1 | const int y=0; |
运算符
算数运算符
运算符 | 描述 | 例子 |
---|---|---|
+ | 加法 | x = y + 2 |
- | 减法 | x = y - 2 |
* | 乘法 | x = y * 2 |
/ | 除法 | x = y / 2 |
++ | 自增 | x = ++y |
x = y++ | ||
– | 自减 | x = –y |
x = y– |
赋值运算符
运算符 | 描述 | 例子 |
---|---|---|
= | 赋值等于 | x = y |
+= | 加等于 | x += y |
-= | 减等于 | x -= y |
*= | 乘等于 | x *= y |
/= | 除等于 | x /= y |
比较运算符
运算符 | 描述 | 例子 |
---|---|---|
== | 等于 | x == 8 |
!= | 不等于 | x != 8 |
> | 大于 | x > 8 |
< | 小于 | x < 8 |
>= | 大于或等于 | x >= 8 |
<= | 小于或等于 | x <= 8 |
条件运算符
语法 | 例子 |
---|---|
布尔 ?值1:值2 | float f=2>3?1.0:2.0; |
逻辑运算符
这里md的table里面我打不出或的||
,就用了俩个1替代
运算符 | 描述 | 例子 |
---|---|---|
&& | 和 | true&&true=true; |
11 | 或 | false11true=true; true11true=true; |
! | 非 | !false=true; |
^^ | 异或 |
向量运算
向量可以与以下数据进行各种运算:
- 单独数字
- 向量
- 矩阵
向量和单独数字的运算
向量可以与单独数字进行加减乘除。
1 | vec4 v=vec4(1,2,3,4); |
上面的计算结果分别为,我这里都是以向量(1,2,3,4)为基底,不是一步步算下来的
向量与数字相加
相当于每位都加上这个数字
1 | (1+1,2+1,3+1,4+1)=(2,3,4,5) |
向量与数字相减
相当于每位都减去这个数字
1 | (1-1,2-1,3-1,4-1)=(0,1,2,3) |
向量与数字相乘
相当于每位都乘以这个数字
1 | (1*2,2*2,3*2,4*2)=(2,4,6,8) |
向量与数字相除
相当于每位都除以这个数字
1 | (1/2,2/2,3/2,4/2)=(0.5,1,1.5,2) |
概念
vec4 是浮点型向量。
我们上例中,写在vec4() 中的整数可以被vec4() 方法转换成浮点数。
vec4 向量在做四则运算时,其用于运算的数字类型应该是浮点型。
比如,下面的写法会报错:
1 | v+=1; |
整型向量ivec2、ivec3、ivec4 的运算原理同上。
向量和向量的运算
1 | vec4 p=vec4(1,2,3,4); |
这次接下来我按照顺序计算
向量相加
两个向量相加就是按位数依次相加
1 | (1+2,2+4,3+6,4+8)=(3,6,9,12) |
向量相减
两个向量相减就是按位数依次相减
1 | (3-1,6-2,9-3,12-4)=(2,4,6,8) |
向量相乘
两个向量相乘就是按位数依次相乘
1 | (2*1,4*2,6*3,8*4)=(2,8,18,32) |
向量相除
两个向量相除就是按位数依次相除
1 | (2/1,8/2,18/3,32/4)=(2,4,6,8) |
向量距离
这里也展开说一下向量距离的计算公式
逻辑
每一位依次按位相减的平方,相加之后开平方
1 | Math.sqrt((2-1)^2+(4-2)^2+(6-3)^2+(8-4)^2) |
GLSL ES简写
1 | distance(p0,p1) 向量距离 |
向量点积
逻辑
每一位相乘并相加,实际上他的值是一个常数,结果就是21+42+63+84=2+8+18+32
GLSL ES简写
1 | dot(p0,p1) 点积 |
向量叉积
逻辑
。向量叉乘公式原理是向量c的方向与a,b所在的平面垂直,且方向要用“右手法则”判断。具体公式为:|向量c|=|向量a×向量b|=|a||b|sin<a,b>
,其中<a,b>
表示向量a和b之间的夹角。
GLSL ES简写
1 | cross(p0,p1) 叉乘 |
向量与矩阵的运算
矩阵只能与向量进行乘法运算。
向量乘以矩阵和矩阵乘以向量的结果是不一样的,但数据类型都是向量。
矩阵乘以向量
1 | mat4 m=mat4( |
以v.x为例说一下其算法:
1 | v.x=m[0][0]*v.x+m[1][0]*v.y+m[2][0]*v.z+m[3][0]*v.w |
可能这个不太好理解,那是因为代码里面是列主序的矩阵,我们把它变成行主序的矩阵,就和我们在大学学的就一样了。
此时的我们四维矩阵就是
1 | 1 2 3 4 |
对应的向量则是
1 | 1 |
我们让矩阵的每一行的值按照顺序相乘并相加,可以得到
1 | 1*1+2*2+3*3+4*4=30 |
得到的结果就是
1 | 30 |
又因为这是我们之前根据行主序算的,所以变成列主序要变成它的转置矩阵,也就是将矩阵的行列互换得到的新矩阵称为转置矩阵,转置矩阵的行列式不变
1 | [30 70 110 150] |
这个就是最终的结果了
矩阵运算
矩阵可以与以下数据进行各种运算:
- 单独数字
- 向量
- 矩阵
矩阵和单独数字的运算
1 | mat4 m=mat4( |
这里和之前的向量部分是一样的,就是每一位都进行加减乘除就可以了,我就不展开了
矩阵和向量的运算
这里在上面向量和矩阵计算说过了,就不多阐述了
矩阵和矩阵的运算
1 | mat4 m=mat4( |
比如我们有上面这样的俩个矩阵(列主序)
矩阵相加
这个很简单,就是把参数按照索引一个个都给加起来
1 | m+=n; |
矩阵减法
相同索引位置的元素相减
1 | m-=n; |
矩阵除法
相同索引位置的元素相除
1 | m/=n; |
矩阵乘法
1 | m*=n; |
这个和上面的向量乘以矩阵是一样的,就是对应行乘以对应列然后相加,这里拿第一列作为参考,列主序的矩阵是每一列乘以每一行,行主序的矩阵是每一行乘以每一列,记住这个就行了
1 | m[0][0]=m[0][0]*n[0][0]+m[1][0]*n[0][1]+m[2][0]*n[0][2]+m[3][0]*n[0][3] |
struct
struct 翻译过来叫结构体,它类似于js 里的构造函数,只是语法规则不一样。
struct 的建立
1 | struct Light{ |
上面的struct 类似于js 的function,color和pos 既是结构体的属性,也是其形参。
struct 的实例化
1 | Light l1=Light( |
上面的vec4()和vec3()数据是结构体的实参,分别对应color属性和pos属性。
访问struct 实例对象中的属性
1 | gl_FragColor=l1.color/255.0; |
数组
glsl 中的数组具有以下特性:
- 属于类型数组
- 只支持一维数组
- 不支持pop()、push() 等操作
在建立某个类型的数组时,在数据类型后面加[]即可,[]中要写数组的长度:
1 | vec4 vs[2]; |
数组的长度需要按照以下方式定义:
1.整形字面量
1 | vec4 vs[2]; |
2.const 限定字修饰的整形变量
1 | const int size=2; |
3.不能是函数的参数
数组需要显示的通过索引位置一个元素、一个元素的赋值。
1 | vs[0]=vec4(1,2,3,4); |
数组中的元素需要用整形的索引值访问
1 | gl_FragColor=vs[1]/255.0; |
程序流程控制
if判断
glsl 中的if 判断和js 里的if 写法是一样的。都有一套if、else if、else 判断。
1 | float dist=distance(gl_PointCoord,vec2(0.5,0.5)); |
if 语句写太多会降低着色器执行速度,然而glsl 还没有switch 语句,大家需要注意一下。
for循环
glsl 中的for循环和js类似;
for(初始化表达式; 条件表达式; 循环表达式){
循环体;
}
for循环的基本规则如下:
循环变量只能有一个,只能是int或float 类型。
在循环体中也可以使用break或continue,其功能和js一样。
1 | float dist=distance(gl_PointCoord,vec2(0.5,0.5)); |
上面的循环变量是整形,我们也可以将其变成浮点型:
1 | for(float f=0.0;f<=4.0;f++){ |
函数
函数的建立
函数类型 函数名(形参){
函数内容;
return 返回值;
}
示例
以颜色置灰的方法为例,演示一下函数的用法。
1 | <script id="fragmentShader" type="x-shader/x-fragment"> |
上面getLum() 便是获取颜色亮度值的方法。
让一个以rgb为分量的向量color与一套置灰的系数vec3(0.2126,0.7162,0.0722) 进行点积运算,便可得到一个亮度值。
1 | dot(color,vec3(0.2126,0.7162,0.0722)); |
以此亮度值为rgb分量值的颜色便是对初始color 的置灰。
1 | vec3 color=vec3(255,255,0); |
函数的声明
我们也可以将函数体放到其调用方法的后面,不过在调用之前得提前声明函数。
其声明方式如下:
1 | 函数类型 函数名(形参类型); |
我们可以基于之前获取亮度的方法写一下:
1 | precision mediump float; |
参数限定词
我们通过参数限定词,可以更好的控制参数的行为。
参数的行为是围绕参数读写和拷贝考虑的。
我们通过参数的限定词来说一下参数行为:
in 参数深拷贝,可读写,不影响原始数据,默认限定词
1 | precision mediump float; |
在上面的主函数体中,我先声明了一个颜色color,我要通过setColor() 方法修改颜色。
当setColor() 方法中的形参color 被in 限定时,就相当于对实参进行了深拷贝,无论我在setColor() 方法里对color 做了什么,都不会影响原始的color 数据。
那么如果我想在setColor() 方法里修改原始的color数据,那就得使用out 限定。
out 参数浅拷贝,可读写,影响原始数据。
1 | void setColor(out vec3 color){ |
const in 常量限定词,只读。
1 | void setColor(const in vec3 color){ |
这样就是错误的
我们只能读取
1 | void setColor(const in vec3 color){ |
注:GLSL ES 还有许多内置方法,比如sin(),cos(),tan,atan() 等,建议大家去官方文档(https://registry.khronos.org/OpenGL-Refpages/es3/) 看一下。
变量的作用域
GLSL ES变量的作用域和es6 类似。
可以通过函数或{} 建立块级作用域,块级作用域内建立的变量就是局部变量。
局部变量只在块级作用域有效。
在函数之外建立的变量就是全局变量。
在代码块内可以直接获取其父级定义域的变量。
变量不存在“变量提升”现象,变量在使用时,需提前声明。
变量不能重复声明。
通过attribute、uniform、varying 限定字声明的变量都是全局变量。
const 可以声明常量,常量是只读的。
精度限定词
精度限定词可以提高着色程序的运行效率,削减内存开支。
精度的分类
webgl 提供了三种精度:
- highp 高精度
- mediump 中精度
- lowp 低精度
一般中精度用得会比较多,因为高精度太耗性能,而且有时候片元着色器会不支持,而低精度又太low。
精度的定义方法
我可以为某个变量设置精度,也可以为某种数据类型设置精度。
比如:
设置某个变量的精度:
1 | mediump float size; |
设置某种数据类型的精度:
1 | precision mediump float; |
注:
着色器中,除了片元着色器的float 数据没默认精度,其余的数据都是有默认精度的。
因此,我们在片元着色器里要提前声明好浮点型数据的精度。
结语
本篇文章就先到这里了,更多内容敬请期待,债见~