4.WebGL+Shader课程基础

WebGL Shader常用外部输入变量

shadertoy变量输入

如果有同学玩过shadertoy一定了解,在这个网站中你可以默认使用一些变量,达到你的目的。经过我最新查阅,目前shadertoy最新外部变量有如下几个:


着色器输入
uniform vec3      iResolution;     // 视口分辨率(以像素为单位)
uniform float iTime; // 着色器播放时间(以秒为单位)
uniform float iTimeDelta; // 渲染时间(以秒为单位)
uniform float iFrameRate; // 着色器帧率
uniform int iFrame; // 着色器播放帧数
uniform float iChannelTime[4]; // 通道播放时间(以秒为单位)
uniform vec3 iChannelResolution[4]; // 通道分辨率(以像素为单位)
uniform vec4 iMouse; // 鼠标像素坐标。xy:当前位置(如果鼠标左键按下),zw:点击位置
uniform samplerXX iChannel0..3; // 输入通道。XX = 2D/Cube
uniform vec4 iDate; // (年、月、日、以秒为单位的时间)

我们在实际中常用的有几个iResolution、iTime与iMouse。因为此网站更加专注于shader的效果,但是我们本教程是基于webgl环境下的shader教程,所以我们需要通过webgl将变量传到shader中。

WebGL Shader变量输入

u_resolution 此变量意思是画布的长和宽

WebGL API中变量赋值

 let resolution = gl.getUniformLocation(gl.program, 'u_resolution');
 gl.uniform2fv(resolution, [512, 512]);

u_time 此变量意思是系统时间

WebGL API中变量赋值

步骤一 获取系统当前时间并赋值
 let utime = gl.getUniformLocation(gl.program, 'u_time');
 let time = (new Date().getTime()) * 0.001;
 gl.uniform1f(utime, time);
步骤二 实时更新时间函数去调取步骤一内容
setTimeout();

u_mouse 此变量意思是系统鼠标位置

步骤一 监听获取鼠标移动事件
canvas.addEventListener('mousemove', mouseMove, true);
//width与height是当前画布的宽和高
function mouseMove(e){
	movex = e.offsetX / width;
	movey = e.offsetY / height;
}
步骤二 赋值给相应的shader变量
let mouse = gl.getUniformLocation(gl.program, 'u_mouse');
gl.uniform2fv(mouse, [movex, movey]);

以上是对于shader输入变量的讨论,至于我们如何应用这些输入的变量创造出效果,后续我们会具体应用,请继续跟我来,baby!

实例

简单讲解下这个例子,这也是我们中级课程中的第一个例子:

1、首先m和p两个变量都是我们上节课给大家讲的将像素坐标进行一个标准化,限定在方便日后操作的区间内:

2、再次t变量是效果的关键,length是一个长度函数,反应的是以鼠标点为中心,根据p点坐标,求得m-p的向量的模。先是这样我们的效果如下面代码所示:(最好配图)图片恰好印证了我们逻辑正确性,越是靠近mose位置说明向量的模越接近与0,越是远离mose,向量的模越是大于1。

precision mediump float;
uniform float u_time;
uniform vec2  u_mouse;
uniform vec2  u_resolution;

void main(void){
    vec2 m =  (u_mouse.xy * 2.0 - u_resolution) / min(u_resolution.x, u_resolution.y);
    vec2 p = (gl_FragCoord.xy * 2.0 - u_resolution) / min(u_resolution.x, u_resolution.y);
    float t = length(m - p);
    gl_FragColor = vec4(vec3(t), 1.0);
}

3、继续我们的理解,此时我们如果扣上sin值,众所周知,sin值最大特点就是返回的值循环变化,(后面我们讲解函数专栏,会给大家详细讲解)。随着m-p向量的模越大,我们sin值是在[-1,1],之间不断震荡,此时p点再某个位置,sin返回的值又恰好等于0,所有有存在黑色带。请结合下面sin函数的图片仔细思考:

在这里插入图片描述

4、*30.的目的自然是让我们sin函数的值震荡区间更加频繁,也就是我们数学上长说的周期更短。

5、至于在加上time*5.的目的是让我们sin函数成波纹状态,动态变化。

precision mediump float;
uniform float u_time;
uniform vec2  u_mouse;
uniform vec2  u_resolution;

void main(void){
    vec2 m =  (u_mouse.xy * 2.0 - u_resolution) / min(u_resolution.x, u_resolution.y);
    vec2 p = (gl_FragCoord.xy * 2.0 - u_resolution) / min(u_resolution.x, u_resolution.y);
    float t = sin(length(m - p) * 30.0 + u_time * 5.0);
    gl_FragColor = vec4(vec3(t), 1.0);
}

坐标系统

回忆WebGL 中的坐标系统

经历了WebGL中坐标变换,最终来到屏幕坐标系,在屏幕坐标系渲染片元像素值的时候就是我们本节课shader的使命。

下图是我将shader中的关键性坐标系做了类比:
在这里插入图片描述

WebGL Shader中坐标系统

gl_FragCoord

gl_FragCoord是片元着色器坐标系,他的原点在左下角,y轴正方向向上延伸,x轴正方向向右延伸。
为了验证此条结论,我们将gl_FragCoord在代码中做了相应的体现。
如右图所示:
左下角颜色值是vec2(0.,0.,0.,1.),第一个和第二参数都是0,所以呈现是黑色满足在原点的规律。
右上角颜色值是vec2(1.,1.,0.,1.),第一个和第二参数都是1,所以呈现是黄色满足规律。
下图是片元着色器中执行代码:

#ifdef GL_ES
precision mediump float;
#endif
uniform vec2  resolution;
void main() {
    vec2 p = gl_FragCoord.xy/resolution;
    gl_FragColor = vec4(p.xy, 0.0, 1.0);
}

接下来我们的目标是将我们gl_FragCoord坐标原点移动到画布的中心,这里我们采用的方法是gl_FragCoord.xy坐标各自减去.5。很多同学对这里存在疑惑:为什么是减去,又为什么是0.5?
在这里插入图片描述

(1)首先为什么是0.5.

vec2 p = gl_FragCoord.xy/resolution;

我们先将坐标系范围明确到[0,1]之间,因为gl_FragCoord默认坐标区间是[resolution.x,resolution.y];
我们常用的是坐标原点在画布中间去进行后续的编程。减去0.5,数值目的是让其范围在[.5,.5]。
(2)为什么是减去呢?
我们以左下角的点为例子,当原点在做下角的时候其点位在坐标是(0,0),如果我们将坐标系移动到原点,那此时改点坐标值会变成(-.5,-.5)。从(0,0)=>(-.5,-.5)中间经历的变换就是横纵坐标分别减去.5.所以是减法。
以此类推,因为范围中所有点都是满足此线性变换,所以都是应该是减去-.5.
*注: vec2 p = gl_FragCoord.xy/resolution;此处存在缺陷,因为在实际情况中画布的宽高比是不等于1的, 如果按照此比例去绘制图形会出现拉伸情况,我们后面再一起讨论如何对此进行优化。

具体的变换过程如下所示,我将逐行,逐字符去解释,

vec2 uv = gl_FragCoord.xy*2./resolution.xy;  
uv-=1.; 
uv.x*=resolution.x/resolution.y; 
步骤一

首先画布区间分别除画布长宽,即可将uv定义在[0.,2.]区间内。

步骤二

将坐标系xy分别减去1.,即可将坐标原点移动到画布的正中心。

步骤三

此处存在缺陷,因为在实际情况中画布的宽高比是不等于1的,如果按照此比例去绘制图形会出现比例拉伸情况。为解决此问题,我们进行以下操作:

uv.x*=resolution.x/resolution.y; 

相对比例大的长度除以比例小的长度,得到的是比值乘以当前比例长的数值所对应的坐标uv.x,其目的是将uv.x变化率与uv.y变化率进行同步。结果如下代码所示:

vec2 uv = gl_FragCoord.xy*2./resolution.xy;  
uv-=1.;
uv.x*=resolution.x/resolution.y; 

如果上述从数学角度去分析,uv.x乘上一个正整数倍数,那么每个单元标量所代表的长度就会拉长,这样就会和y轴单元标量变化比率保持一致。

**注:**这里的 uv-=1.;uv.x*=resolution.x/resolution.y;是不能颠倒位置的。绿色和橙色的线是本应该移动距离(-1.,1.),但是如果我们假设先让我们uv.x乘上某个值,比如点(0.3,0)那么此时点的坐标发生变化,若此时再去移动(-1,1.),原点(绿色点)的位置就不在中间位置。

在这里插入图片描述

步骤四

以上基本已经达到我们的目标,但是犹豫其代码复杂度较高,我们进行以下简写:

vec2 p = (gl_FragCoord.xy*2. - resolution.xy) / min(resolution.x, resolution.y);

这里面应用 min(resolution.x, resolution.y)的目的是将uv.x与uv.y的变化率进行了同步,并且按照最小的比例长度进行计算。如下图所示:橙色的长方形是当前画布的颜色,绿色是修正后的着色器绘制区。

在这里插入图片描述

**注:**实际上我们uv坐标进行了坐标系转换,我们在之后内容中提到mouse鼠标点也可以应用此方法,将值的变化范围定格你想的区间。但是mouse坐标系和gl_FragCoord坐标系的原点位置和y轴方向大家一定要注意区分哈。

let mouse = (u_mouse.xy * 2.0 - u_resolution) / min(u_resolution.x, u_resolution.y);

这是不是非常cool,实际上这为我们后续的操作带来极大方便(还记得我们在初级课程中给大家讲到rgb颜色范围是在[0,1]区间内哦,我们这样做其实也方便了我们将uv位置关系和颜色做一个挂钩,也方便我们去检索逻辑上的bug,以后你或许会更加明白!*

gl_PointCoord

gl_PointCoord坐标原点是在点的左上角,同样我们为了验证结论,我们采用如下代码进行测试:

#ifdef GL_ES
precision mediump float;
#endif
uniform vec2  resolution;
void main() {
    gl_FragColor = vec4(gl_PointCoord.xy, 0.0, 1.0);
}

通过下图我们可以得到左上角的颜色值是vec2(0,.,0.),右下角颜色值vec2(1.,1.),这样可以验证左上角坐标即为我们的0,0点咯。

二者关系

二者之间的关系是相对的,同一个像素点在不同的坐标系范围内,被表示的值也是不一样的,因为二者参考坐标系是不一样的。

WebGL shader颜色计算

自然界中的颜色

自然界中主要有3种颜色分别是R(红)G(绿)B(蓝)这3种是最基本的颜色,称为三基色。它的颜色混合方式就好像有红、绿蓝三盏灯,当它们的光相互叠合的时候色彩相混,而亮度却等于两者亮度之总和,越混合亮度越高。红、绿、蓝三个颜色通道每种色各分为255阶亮度在0时“灯”最弱时候是关掉的,而在255时“灯”最亮。当三色数值相同时为无色彩的灰度色而三色都为255时为最亮的白色都为0时为黑色。同时颜色会相加,并向中间靠拢,最终形成新的色系。
请添加图片描述

这里介绍一个中国色的网站,网站做的非常出色,有兴趣的朋友学习。http://zhongguose.com/#danfei
请添加图片描述

Shader中的颜色

shader中颜色是对自然界中颜色的仿真与模拟,具有和自然界中颜色类似的效果。以下将shader中颜色特性做解释:
1.颜色分为红®,绿(g),蓝(b),透明度(a)是个值,组合为个4维向量(r,g,b,a)。
2.每个分量的范围是0->1的浮点数,即颜色值/255
3.可以每个分量进行加减乘除计算,也可对整个颜色向量进行加减乘除计算。
常用颜色距离:

(0.0,0.0,0.0,0.0): 黑色 
(1.0,1.0,1.0,1.0): 白色
(1.0,0.0,0.0,1.0): 红色
(0.0,1.0,0.0,1.0): 绿色
(0.0,0.0,1.0,1.0): 蓝色

Shader颜色计算

请添加图片描述

左边颜色可以表示为:

color1
vec3(1.) 
vec3(0.)

右边颜色可以表示为:

color2
vec3(1.,1.0.)
vec3(0.)

问题:想从左边颜色转换为右边颜色如何计算呢?
答:
左边颜色乘上vec3(1.,1.0.)即可转为右边颜色。
因为在shader里面 黑色乘上任何数字都是黑色,白色乘上任何数字都是乘上的这个数所表示的颜色。

Shader颜色计算常见问题

经常遇到问题:
1、底色比如是黑色,无论乘上什么数都还是黑色;
2、如果是白色,乘以什么就是他本身。
3、底色是黑色,加上什么颜色就是颜色本身。
4、如果颜色系数小于0,那么返回的效果也都是黑色。
5、如果颜色系数大于1,那么返回的效果也都是白色。
注:shader的核心在于数学公式,在于数字的加减乘除,我们的学习过程一定是不断的学习数学,并且总结数学,将规律封装成关键公式去使用。

WebGL Shader颜色输出与打印

为什么要打印颜色?

想一想我们在应用这些语言在打印的时候目的是为了什么呢?答案是调试。以下将不同语言中的打印进行了枚举,供大家欣赏:

Java:System.out.println();
C#:Debug.WriteLine();
JS:console.log();

但是Shader里面目前没有console.log(),没有alert(),更没有什么debugger,也没有F12开发者模式,因为shader的特殊性(GPU渲染完数据在显存,回传内存的唯一方式glReadPixels函数),我们之前调试工具都是在CPU里面进行,目前GPU的调试工具实现比较难,我相信未来一定可以实现变量数值的调试!也许那时候三维编程更加简单!
所以只能通过观察颜色变化,来获取某一节点的数值,通过数值来观察程序的异常,说实话这种情况在实际应用不是很多,大多数通过函数图形或者逻辑推演(我们在中级课程中给大家讲解)的方式进行。但是颜色打印对于毫无头绪的bug,真的是很实用的工具!

如何打印颜色值?

在这里插入图片描述
我们以webgl1为例子:
像素读取的时候是按照一个个小长方形进行读取,明确长方形左下角x,y坐标系,在确定起长和高,即可明确长方形的大小,进而将整个读取的数据放到提前声明好的类型化数组里面。
x:指定从矩形像素块的左下角。
y:指定从矩形像素块的左下角。
width:指定矩形宽度。
height:指定矩形高度。
format:指定像素数据格式。gl.ALPHA 、gl.RGB、gl.RGBA。
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_5gl.UNSIGNED_SHORT_4_4_4_4gl.UNSIGNED_SHORT_5_5_5_1
Float32Array  => .gl.FLOAT

常见异常

如果不是可接受的值,则会引发错误。gl.INVALID_ENUMformattype
如果出现以下情况,则会引发错误。gl.INVALID_OPERATION
type是和不是。gl.UNSIGNED_SHORT_5_6_5formatgl.RGB
type是和不是。gl.UNSIGNED_SHORT_4_4_4_4formatgl.RGBA
type与 的类型化数组类型不匹配。pixels
如果当前 绑定帧缓冲未完成。gl.INVALID_FRAMEBUFFER_OPERATION

WebGL Shader 变量、矢量、矩阵、结构体、数组、函数、限定词、预处理

变量

变量定义

GLSL ES 语言的变量,需要遵循以下原则:

  1. 变量名只能包含英文字符、数字和下划线即(a-z、A-Z、0-9、_)
  2. 变量名的首字母不能是数字
  3. 不能以 gl_ 、webgl_ 或 webgl 开头,这些前缀已经被OpenGL ES 保留了
  4. 不能是GLSL ES内置的关键字和保留的关键字
GLSL ES 关键字
attributeboolbreakbvec2bvec3bvec4
constcontinuediscarddoelsefalse
floatforhighpifininout
intinvariantivec2ivec3ivec4lowp
mat2mat3mat4mediumoutprecision
returnsampler2DsamplerCubestructtrueuniform
varyingvec2vec3vec4voidwhile
GLSL ES 保留字
asmcastclassdefault
doubledvec2dvec3dvec4
enumexternexternalfixed
flatfvec2fvec3fvec4
gotohalfhvec2hvec3
hvec4inlineinputinterface
longnamespacenoinlineoutput
packedpublicsampler1Dsampler1DShadow
sampler2DRectsampler2DRectShadowsampler2DShadowsampler3D
sampler3DRectshortsizeofstatic
superpswitchtemplatethis
typedefunionunsignedusing
volatile

变量基本类型

基本类型

GLSL ES中变量类型中的基本类型主要有布尔类型、整形和浮点型。

变量类型说明
bool布尔类型,该类型的变量表示一个布尔值即 true或false
int整型,该类的变量表示一个整数
float单精度浮点数类型,该类型的变量表示一个单精度的浮点数
类型转换

使用等号(=)可以将值赋给变量,GLSL ES 是强类型语言,如果等号左右两边的变量类型不一样,会报错的哈。例如:在语义上 1 和 1.0 是一个值,但是将 1 赋值给浮点型变量时会出错。

转换函数描述
转换为整型数int(float)去掉浮点数小数部分,转换为整型数
int(bool)true 转换为1,false 转换为0
转换为浮点点float(int)将整型数转换为浮点数
float(bool)true 转换为1.0,false转换为0.0
转换为布尔值bool(int)0转换为false,非0转换为true
bool(float)0.0 转换为false,非0转换为 true

矢量

矢量类型

向量指的是具有大小(Magnitude)和方向(Direction)量。在物理学中也称为矢量。向量的表示通常使用小写字母上面加上向右的箭头→表示,或者使用粗体的小写字母表示。向量具有平移不变形。向量只与大小和方向有关系,和向量的起点和终点没有关系。向量也只包含两个属性:大小和方向。

对于空间中的两个点𝐴和𝐵,从𝐴到𝐵的向量可以表示为𝐴𝐵,计算方法为𝐴𝐵=𝐵−𝐴。这块大家要留意一下,就是向量的表示方法和实际的顺义是相反的哈(我记得高中时候数学老师经常强调这一点,不然有的时候求出来的角度是不对的)。

请添加图片描述

GLSL ES支持矢量类型,这种数据类型适合用来处理计算机图形。矢量类型的变量包含多个元素,每个元素是一个数值(整型数、浮点数或布尔值)。矢量将这些元素排成一列,可以用来表示顶点坐标或颜色值等。

矢量类型介绍
变量类型说明
vec2, vec3, vec4具有2、3、4个浮点数元素的矢量
ivec2, ivec3, ivec4具有2、3、4个整形元素的矢量
bvec2, bvec3, bvec4具有2、3、4个布尔值元素的矢量
向量代数表示

o a ⃗ = [ 4 3 ] \vec{oa} = \left[\begin{matrix} 4 \\ 3 \\ \end{matrix}\right] oa =[43]
如果此时求向量oa的转置:
o a ⃗ = [ 4 3 ] \vec{oa} = \left[\begin{matrix} 4 & 3 \\ \end{matrix}\right] oa =[43]

矢量构造、赋值与取值
构造

在GLSL ES中,矢量非常重要,所以GLSL ES提供了丰富灵活的方式来创建矢量。请重点体验下下面的构造方法

vec3 v3 = vec3(1.0,1.2,1.5); //传入三个分量
vec2 v2 = vec2(v3);  //传入单个分量,其他自动填充
vec4 v4 = vec4(1.0);  vev4 v4b = vec4(v2,v4); //两个变量拼成一个变量,第一个变量取全部的值,第二个变量取剩下的值填满vec4

**注:**如果向构造函数中只传一个参数,构造函数会自动将这个参数赋值所有元素,但是如果给构造函数传了不止一个值,但又比所需的参数个数少,就会报错。

赋值

使用等号(=)来对矢量进行赋值操作。记住,赋值运算符左右两边的变量/值的类型必须一致,左右两边的元素个数也必须相同。赋值情况比较灵活,但大家可以自行尝试,体会下面代码的含义。

vec3 v3 = vec3(1.0,1.2,1.5); //三维矢量v3的设置为(1.0,1.2,1.5),即v3的三个分量x、y、z分别是1.0,1.2,1.5
vec2 v2 = vec2(v3); //使用三维矢量v3赋值给二维矢量v2,实际是使用v3的前两个元素给v2赋值
vec4 v4 = vec4(1.0); //将四位矢量v4设置为(1.0,1.0,1.0,1.0)
vev4 v4b = vec4(v2,v4); //使用v2和v4赋值v4b,结果为(1.0,1.2,1.0,1.0)
取值

矢量既可以通过点运算符(.)来访问也可以通过方括号运算符([])来访问。通常矢量通过点运算符来访问,只需要在矢量变量名后接点运算符,然后在接上分量名。

x,y,z,w 	用来获取顶点坐标的分量 
r,g,b,a 	用来获取颜色分量 
s,t,p,q 	用来获取纹理坐标的分量 

矢量可以用来存储顶点坐标、颜色和纹理坐标。所以GLSL ES支持以上三种分量名,这样大家在使用的时候便于理解,实际上,一个矢量的x分量或r分量还是s分量都会返回这个矢量的第1个分量,一个矢量的y分量或g分量还是t分量都会返回这个矢量的第2个分量。

**注:**这点实际上和咱们前端javascript很类似哈。这块需要大家留意的是需要多个分量搭配使用的方法,进行灵活运用

矢量常见用法
向量长度

∣ a ∣ = x 2 + y 2 + z 2 \left|a\right| = \sqrt{x^{2} + y^{2}+ z^{2}} a=x2+y2+z2

所谓的向量长度就是向量长度,你们懂的。是不是有点“听君一席话,胜似一席话”。开个玩笑,向量的长度应该就是它的模长,那它的模长又是何物呢,就是他的对应的线段长度。(不严谨哈,更加严谨的定义肯定得去查阅数学课本),上式子实际上是用代数式子表示模的长度。

向量归一化===单位向量

向量的大小(长度)称为向量的模(norm),一般记作
∣ A ∣ = x 2 + y 2 / / s h a d e r 中用法: l e n g t h ( ) \mid A\mid = \sqrt{x^2+y^2} //shader中用法:length() A∣=x2+y2 //shader中用法:length()
单位向量(Unit vector)指的是模为1的向量。单位向量的计算公式为:
a ^ = a ⃗ ∣ a ⃗ ∣ \hat{a} = \frac{\vec{a}}{\mid \vec{a} \mid } a^=a a
单位向量的长度为 1,一般只用来表示方向。

注:shader中用法:normalize()

向量加法与减法

向量的求和可以用两种法则:平行四边形法则或者三角形法则。

请添加图片描述

在代数上,对于在笛卡尔坐标系中定义的向量,向量求和可以简化为求向量各个对应坐标值的和。

**注:**shader中用法:vec2 f +=vec2 (1.,1.)

4.向量乘法

1)点乘(Dot product),又称为向量的内积。计算公式为:
a ⃗ . b ⃗ = ∣ a ⃗ ∣ ∣ b ⃗ ∣ c o s θ \vec{a} .\vec{b} = \mid \vec{a} \mid \mid \vec{b} \mid cos\theta a .b =∣a ∣∣b cosθ
代数形式如下所示:

在这里插入图片描述

点乘满足交换律以及分配律。
a ⃗ . b ⃗ = b ⃗ . a ⃗ \vec{a} .\vec{b} = \vec{b} .\vec{a} a .b =b .a

a ⃗ . ( b ⃗ + c ⃗ ) = a ⃗ . b ⃗ + a ⃗ . c ⃗ \vec{a} .(\vec{b} +\vec{c} )= \vec{a} .\vec{b}+\vec{a} .\vec{c} a .(b +c )=a .b +a .c

**注:**shader中用法:float f =dot(vec2 (0.,2.),vec2 (0.,1.))

实际应用

(1)计算两个向量夹角
cos ⁡ θ = a ⃗ . b ⃗ ∣ a ⃗ ∣ ∣ b ⃗ ∣ \cos\theta = \frac{\vec{a} .\vec{b}}{\mid \vec{a} \mid \mid \vec{b} \mid } cosθ=a ∣∣b a .b

cos ⁡ θ = a ^ . b ^ \cos\theta = \hat{a} .\hat{b} cosθ=a^.b^

(2)计算一个向量到另一个向量上的投影 ,向量b在向量a上的投影满足,注意此时的投影是个标量

下面这个是根据三角形定则来的,得到的值是下面公式:
k = ∣ b ⃗ ∣ cos ⁡ θ k=|\vec{b}|\cos\theta k=b cosθ
再把上面公式的cos角度置换了,得到结果如下所示
k = a ⃗ . b ⃗ ∣ a ⃗ ∣ k=\frac{\vec{a} .\vec{b}}{\mid \vec{a} \mid } k=a a .b

2)叉乘(Crossproduct),又称作向量的外积。两个向量叉乘的结果还是一个向量,这个向量和原来的两个向量垂直。叉乘的结果向量的长度为:
∥ a × b ∥ = ∥ a ∣ ∥ b ∥ sin ⁡ ϕ \|a \times b\|=\|a \mid\| b \| \sin \phi a×b=absinϕ
如果代数上表示的话如下公式所示:
在这里插入图片描述

几何意义

在几何中,叉积得到的向量与a和b所在平面垂直,长度等于向量a和b组成的平行四边形的面积,该向量被称为法向量。如图:

请添加图片描述

法向量方向:使用右手定则,首先伸出右手,并竖起大拇指,并将其余的四个手指握紧。以最小角度旋转向量a,使其与向量b的方向一致,四个手指朝向如上图,大拇指所指的方向就是法向量的方向。

请添加图片描述

**注:**shader中用法:vec2 f =cross (vec2 (.1,.2,.3 ),vec2 (.3,.2,.1))

矩阵

矩阵定义

矩阵(Matrix)是一个按照长方阵列排列的复数或实数集合 ,最早来自于方程组的系数及常数所构成的方阵。这一概念由19世纪英国数学家凯利首先提出。
GLSL ES支持矩阵类型,这种数据类型适合用来处理计算机图形。矩阵类型的变量包含多个元素,每个元素是一个数值(整型数、浮点数或布尔值)。矩阵将这些元素划分成行和列,可以用来表示变换矩阵。

矩阵类型介绍

GLSL ES常见矩阵类型如下所示:

mat22×2 的浮点数矩阵
mat33×3 的浮点数矩阵
mat44×4 的浮点数矩阵
行主序和列主序

在学习矩阵构造和赋值前先搞明白两个概念,按行主序和按列主序。WebGL中矩阵元素是按列主序存储在数组中的。
在这里插入图片描述

在WebGL中使用的矩阵都是按列主序的,例如你用构造函数创建一个4×4的矩阵时,向矩阵构造函数中传入矩阵的每一个元素的数值来构造矩阵,注意传入值的顺序必须是列主序的。

请添加图片描述

左乘

请添加图片描述

右乘

在这里插入图片描述

矩阵构造、赋值与取值

构造

创建普通的矩阵

mat4 mat44 = mat4(
    1.0, 2.0, 3.0, 4.0,
    5.0, 6.0, 7.0, 8.0,
    9.0, 10.0, 11.0, 12.0,
    13.0, 14.0, 15.0, 16.0
);

创建对角矩阵或者单位矩阵

mat4 matrix = 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
取值
mat4 mat44 = mat4(
    1.0, 2.0, 3.0, 4.0,
    5.0, 6.0, 7.0, 8.0,
    9.0, 10.0, 11.0, 12.0,
    13.0, 14.0, 15.0, 16.0
);

// 访问矩阵matrix44的第二列vec4 v4 = matrix44[1];//返回值vec4(5.0, 6.0, 7.0, 8.0,)

// 访问矩阵matrix44的第二列第一行对应的元素float f = matrix44[1][0];//返回5.0

矩阵常见用法

矩阵和浮点数相乘

mat4 mat44 = mat4(
    1.0, 2.0, 3.0, 4.0,
    5.0, 6.0, 7.0, 8.0,
    9.0, 10.0, 11.0, 12.0,
    13.0, 14.0, 15.0, 16.0
);
mat4 m44 = matrix44*10.;
//结果是
mat4 m44 = mat4(
    10.0, 20.0, 30.0, 40.0,
    50.0, 60.0, 70.0, 80.0,
    90.0, 100.0, 110.0, 120.0,
    130.0, 140.0, 150.0, 160.0
);

矩阵与点或(矢量)相乘

//声明一个点
vec4 point = vec4(0.,1.,1.,1.);
//向量扩大3倍
//3   0   0    0            0.
//0   3   0    0            1.
//0   0   3    0            1.
//0   0   0    1            1.
mat4 m44 = mat4(3,0,0,0,  0,3,0,0,  0,0,3,0,  0,0,0,1);
vec4 newPoint = m44*point ;
//平移后结果:newPos = vec4(0.,3.,3.,1.)


矩阵和矩阵相乘

1、当矩阵A的列数(column)等于矩阵B的行数(row)时,A与B可以相乘。

2、矩阵C的行数等于矩阵A的行数,C的列数等于B的列数。

3、乘积C的第m行第n列的元素等于矩阵A的第m行的元素与矩阵B的第n列对应元素乘积之和。

在这里插入图片描述

结构体定义

着色器中的机构体类似于我们其他语言中的类,GLSL的结构体可以组合基本类型和数组来形成用户自定义的类型。在定义结构体的时候,你可以直接定义一个结构体实例,或者在后面定义结构体实例。

方法一:

struct Light{
  vec3 position;//光源位置
  vec4 color;//光源颜色
}; light1,light2
// Light light1=Light(vec3(1.,2.,3.),vec4(8.,2.,4.,1.));

结构体访问与使用

1、=为结构体赋值,

2、==,!=来判断两个结构体是否相等。只有结构体中的每个成分都相等,那么这两个结构体才是相等的。

3、访问结构体的内部成员使用. (点语法)来访问。

4、固定大小的数组也可以被包含在结构体中。GLSL的结构体不支持嵌套定义。只有预先声明的结构体可以嵌套其中。

结构体注意事项

1、GLSL的结构体不支持嵌套定义,只有预先声明的结构体才可以嵌套其中。

2、结构体定义后面一定要加分号。

3、结构体定义是和main函数并列的。

4、结构体至少包含一个成员。

struct theTemp
{
    float pointSize;
    myTemp temp;
    //注意,这里是不合法的,不可以这么写。结构体只有预先定义过的才可以嵌套。
    struct cantUse{
        float a;
        float b;
    }test;
    //这也是个结构体,但是我们是在这个结构体的下面定义的,所以也是不合法的。
    subTemp sub;
}
struct subTemp
{
    float c;
    float d;
}tex;

数组

数组定义

在GLSL中,可以像C一样声明和访问数组,只支持一维数组,下标不能为负。

//声明含有4个数浮点数的数组 
float Array1[4];
//声明含2个vec4的对象数组
 vec4 Array2[2];

注意:

1、整型字面量

2、数组的长度必须大于 0;

3、数组前面定义的是每个子量的类型

数组构造、赋值与取值

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

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

数组元素可以通过索引值来访问,和JS等语言一样,索引值也是从0开始的。比如,下面这句代码就可以访问float Array变量的第3个元素:

float aa = Array[2];

数组运算

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

//将floatArray的第2个元素乘以3.14    float f = Array[1] * 3.14    

//将vec4Array的第1个元素乘以vec4    vec4 v4 = vec4Array[0] * vec4(1.0, 2.0, 3.0, 4.0); 

限定词

GLSL ES 中提供了三类限定词,主要是为了提高运行效率,减少内存开销。

参数限定词—控制参数的行为
存储限定词—限定变量的类型
精度限定词—限定数据的精度

参数限定词

参数限定词顾名思义就是来限制函数参数的,根据参数不同的行为可以将它们分为以下几类:

● 只向函数中传值
● 在函数中被赋值
● 既向函数中传值也在函数中被赋值

存储限定词

存储限定词其实就是我们用来向着色器传值的变量类型,GLSL ES提供了attribute、uniform和varying三个限定词来声明特定用途的变量。
在这里插入图片描述

GLSL ES支持的存储限定词最大最小个数

在这里插入图片描述

attribute变量

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

uniform变量

uniform变量可以用在顶点着色器和片元着色器中,且必须是全局变量。uniform变量是只读的,不支持数组和结构体类型。

varying变量

varying限定词声明的变量也要求是全局变量,它担任着一个很重要的角色,就是从顶点着色器向片元着色器传值,

精度限定词

GLSL ES新引入了精度限定字,目的是帮助着色器程序提高运行效率,削减内存开支。 精度默认值写法:

#ifdef GL_ES
precision mediump float;
#endif

WebGL程序支持三种精度,限定词分别是 highp、mediump和lowp

通常会使用 precision 关键字为某一类型的变量设置默认精度,这个设置必须放置在程序的顶部。使用方式如下: p r e c i s i o n < 精度限定词 > < 类型名称 > precision <精度限定词> <类型名称> precision<精度限定词><类型名称> 默认精度限定参照表:

函数

函数结构

GLSL ES 的函数定义与C语言接近,基本构成包括返回类型、函数名、参数和函数体,若无返回值,需要使用 void 代替。具体结构如下 :

返回类型 函数名 (type0 arg0, type1 arg1, ...,typen argn) {
    函数计算
    return 返回值;
}

参数的type必须为本章所述的类型之一,或者像main()函数这样没有参数也是允许的。如果函数不返回值,那么函数中就不需要有return语句。但这种情况下,返回类型必须是void。你也可以将自己定义的结构体类型指定为返回类型,但是结构体的成员中不能有数组。

注:使用函数时传入的参数类型必须与声明函数时指定的参数类型一致。在GLSL中函数不能在函数内部调用它本身,也就是说不允许递归调用,这项限制的目的是为便于编译器对函数进行内联展开。

规范声明

与C语言一样,如果函数定义在其调用之后,那么必须在进行调用前先声明该函数的规范,规范声明预先告诉GLSL系统函数的参数、参数类型、返回值等信息。

流程控制

着色器中的流程控制与C和JavaScript语言中的流程控制几乎无异,主要是通过if语句和for语句等控制流程。

if语句

if语句有三种控制流程的语句模型,分别是 if 模型、 if…else… 模型 和 if…else if…else 模型。

if(条件表达式1){
    如果条件表达式1true,执行这里。
} else if(条件表达式2)
{
    如果条件表达式1false而条件表达式2true,执行这里。
} else {
    如果上述两个条件都为false执行这里。
}

注:if语句或if-else语句中都必须包含一个布尔值,或者是产生布尔值的表达式。此处不可以使用布尔值类型矢量,比如bvec2。GLSL ES中没有switch语句,在程序中避免过多的使用if语句,否则会影响效率

for语句

大多数循环程序都是通过for语句实现的,GLSL ES 语言也一样,for 语句的格式如下:

for (初始化表达式;条件表达式;循环步进表达式){
    重复执行的语句;
}

GLSL ES 语言的for循环的循环变量有一些特殊的限制,具体如下:

● 一个for循环中只允许有一个循环变量,且只能是int类型或float类型;

● 循环步进表达式必须是以下的形式中的一种,i++,i–,i+=常量表达式,i-=常量表达式;

● 条件表达式必须是循环变量与整形常量的比较;

● 循环体内,循环变量不可以被赋值。

continue、break语句

通常情况下,我们会在for语句中使用continue语句来跳过本次循环,使用break语句终止循环。

continue: 中止包含该语句的最内层循环和执行循环表达式(递增/递减循环变量),然后执行下一次循环。

break:中止包含该语句的最内层循环,并不再继续执行循环。

discard语句

discard语句只能在片元着色器中使用,表示放弃当前片元直接处理下一个片元,作用与continue类似。

预处理

预处理指令用来在着色器程序真正编译之前对代码进行预处理,预处理指令都是以 (#) 开始。当我们编写 GLSL 代码时,编译器需要先对代码进行处理,然后才能将其编译成机器可以执行的指令。预处理指令就是在这个处理过程中对代码进行一些操作。

#define

使用 #define 指令定义常量,可以在代码中使用该常量来提高代码的可读性和维护性。

#ifdef, #ifndef, #else, #elif, #endif

根据不同的条件选择编译不同的代码。这样可以让代码更加灵活,便于开发人员进行调试和测试。

#version

指定当前代码所使用的 GLSL 版本,确保代码与特定版本的 GLSL 兼容。必须在着色器顶部,在它之前只能有注释和空白。

#warning 和 #error

在编译期间生成警告和错误信息,帮助开发人员调试和诊断代码问题。

#extension

预处理指令中另一个非常重要的是 #extension,用来控制是否启用某些扩展的功能。

#extension extension_name : enable/disable

预处理指令可以让我们更好地组织代码,提高代码的可读性和可维护性,同时也能够帮助我们调试和诊断代码问题。

WebGL Shader内置变量和常量

内置变量

在这里插入图片描述

内置常量

在这里插入图片描述

OpenGL OpenGL ES WebGL GLSL 版本

GLSL ES 是用于在 WebGL 中编写顶点和片段着色器代码的语言。WebGL2.0支持较新版本的GLSL,称为GLSL3,而WebGL1.0仅支持GLSL1。GLSL3 提供了以前 GLSL1 不支持的新类型、运算符和函数,并且用于从着色器程序导入、导出数据的语法略有不同。
在这里插入图片描述
注:webgl里面读取版本的方式是:

  console.log(`${gl.getParameter(gl.VERSION)}与${gl.getParameter(gl.SHADING_LANGUAGE_VERSION)}`);

GLSL ES版本差异

版本 100

顶点着色器
#version 100 es
uniform mat4 projTrans;
attribute vec2 Position;
attribute vec2 TexCoord;
varying vec2 vTexCoord;

void main() {
    vTexCoord = TexCoord;
    gl_Position = u_projView * vec4(Position, 0.0, 1.0);
}
片段着色器
#version 100
uniform sampler2D tex0;
varying vec2 vTexCoord;
void main() {
    vec4 color = texture2D(tex0, vTexCoord);
    gl_FragColor = color;
}

版本 300

顶点着色器

#version 300 es
in vec4 a_position;
uniform vec4 u_offset;
 out vec4 v_positionWithOffset;
 void main() {
  gl_Position = a_position + u_offset;
  v_positionWithOffset = a_position + u_offset;
}

片段着色器
#version 300 es
precision highp float;
in vec4 v_positionWithOffset;
out vec4 outColor;
void main() {
 vec4 color = v_positionWithOffset * 0.5 + 0.5;
  outColor = color;
}

参考地址:https://registry.khronos.org/OpenGL/index_es.php#specs32

小结

章节复习

现在我们来检验你对这结课程的理解?

  • 手写两个矩阵相乘
A = [[1, 2, 3],
     [4, 5, 6]]

B = [[7, 8],
     [9, 10],
     [11, 12]]
C = [[58, 64],
     [139, 154]]
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值