零基础玩转 WebGL - 着色器
本文为稀土掘金社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!
这篇文章将详细讲解 WebGL1 中的着色器。
WebGL 基于 OpenGL,是 OpenGL 的子集。OpenGL 中的着色器是使用 GLSL 编写的,所以 WebGL 中也是使用 GLSL 编写着色器,WebGL1 中使用的 GLSL 1.00 版本。
GLSL(OpenGL Shading Language)是 OpenGL 着色器语言,是一个以 C 语言为基础的高阶着色语言。它可以提供开发者对渲染管线更多的控制,而无需使用汇编语言或硬件规格语言。
上一篇文章介绍了 WebGL 程序执行主要分为两个阶段,CPU 阶段和 GPU 阶段,在 CPU 中我们可以直接使用 JS 来编写代码,但是如果要控制 GPU 的渲染逻辑就需要使用着色器,也就是使用 GLSL 编写的着色器程序。
GPU 中的处理过程大致如下图,其中蓝色部分是我们可以控制的(忽略几何着色器,WebGL中没有)。
image.png之前有介绍过,WebGL 只能渲染点、线和三角形,那些复杂的 3D 模型都是一个个三角形组成的。一个三角形是由 3 个顶点组成,如果我们想渲染两个三角形,就提供 6 个顶点,WebGL 每处理完 3 个顶点后会将这三个顶点连接成一个三角形。
WebGL 中一共有两种类型的着色器,分别是顶点着色器和片段着色器,一个处理每个顶点位置,一个处理每个片段的颜色。
顶点着色器顶点着色器(vertex shader)用来处理每个顶点,计算出每个顶点的位置,比如要渲染一个三角形,它是由 3 个顶点组成的,那么顶点着色器就会运行 3 次,分别处理 3 个顶点的位置。
下面是一个最简单的顶点着色器。
attributevec4a_position;
voidmain(){
gl_Position=a_position;
}
WebGL 会默认执行着色器中的main函数,顶点着色器中我们使用attribute存储限定字获取外部传入的顶点信息。然后使用gl_Position(内置变量)输入处理过的顶点位置(比如在顶点着色器中将顶点左移 10px)。
顶点着色器中要计算顶点的坐标,计算好的坐标并不是在main函数中直接返回,而是赋值给内置的gl_Position变量。上面例子中我们直接 JS 中传递过来的数据作为顶点位置。(如果不清楚数据怎么传递的请查看上篇文章)
我们可以使用下面的伪代码表示顶点着色器是如何被执行的。
constpoints=[
0,0.5,
0.5,0,
-0.5,-0.5
]
//三角形的三个顶点,在CPU中提供的数据
constvertex=points.map(p=vertexShader(p))
//获得最终的顶点位置
如上图所示,我们从外部输入顶点坐标,然后在顶点着色器中对它进行矩阵运算,然后通过gl_Position变量输出新的坐标。(矩阵变换请查看后续文章)
attribute 存储限定字上面例子中有一个attribute关键字,它只能用在顶点着色器,被用来表示逐顶点信息,一般用来传递顶点位置、顶点颜色等信息。我们可以通过它来获取外部传递过来的信息。上面例子中,我们定义了三个顶点传递给a_position变量,顶点着色器不是一次性获取到这些顶点,而是一个个的获取。
上篇文章中我们是这样向顶点着色器传递数据的。
constpositionLocation=gl.getAttribLocation(program,'a_position')
constpositionBuffer=gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER,positionBuffer)
gl.bufferData(gl.ARRAY_BUFFER,newFloat32Array([
0,0.5,
0.5,0,
-0.5,-0.5
]),gl.STATIC_DRAW)
gl.vertexAttribPointer(positionLocation,2,gl.FLOAT,false,0,0)
我们首先获取变量a_position的位置。由于我们需要传递一堆的顶点,所以这里创建一个 Buffer 来存放这些顶点。我们还需要使用vertexAttribPointer告诉 WebGL 如何获取 Buffer 中的数据。
片段着色器片段着色器(fragment shader)也叫片元着色器,在顶点着色器处理完顶点后,WebGL 还会把这些三角形进行插值,将三角形变成一个个像素,然后对每个像素执行一次片段着色器,片段着色器中使用gl_FragColor内置变量输出这个像素的颜色。
上篇文章中的片段着色器代码如下。
precisionmediumpfloat;
uniformvec4u_color;
voidmain(){
gl_FragColor=u_color;
}
uniform 存储限定字我们先忽略比顶点着色器多出的一行代码precision mediump float;。我们在片段着色器中使用了uniform存储限定字获取了外部传递的数据。
uniform存储限定字,可以用在片段着色器也可以用在顶点着色器,它是全局的,在着色器程序中是独一无二的。
上篇文章我们是这样传递数据到片段着色器中。
constcolorLocation=gl.getUniformLocation(program,'u_color')
gl.uniform4f(colorLocation,0.93,0,0.56,1)
由于uniform是全局唯一的,相当于常量,所以不需要 Buffer,直接设置它的值就行了。uniform4f方法后缀4f表示 4 个浮点数,在这里我们用来表示一个颜色值的rgba。
GLSL 语法接下来将讲解 GLSL 语法,当然并不会一次性全部讲完,会先介绍最常用的一部分,后续文章再慢慢讲解,如果你想了解更多信息,请查看这个文档,https://www.khronos.org/registry/OpenGL/specs/es/2.0/GLSL_ES_Specification_1.00.pdf
GLSL 它是强类型语言,每一句都必须有分号。它的语法和 typescript 还挺像。
GLSL 的注释语法和 JS 一样,变量名规则也和 JS 一样,不能使用关键字,保留字,不能以gl_、webgl_或_webgl_开头。运算符基本也和 JS 一样,++--+=&&||还有三元运算符都支持。
GLSL 中主要有三种数据值类型,浮点数、整数和布尔。注意浮点数必须要带小数点。类型转换可以直接使用float、int和bool函数。例如下面像下面这样将整数1转换成浮点数1.0。
floatf=float(1);
另外 GLSL 中定义变量的语法是 变量名,并且赋给变量的值应该和定义变量的类型是一致的,如果上面代码这样写float f = 1;将会直接报错,因为1是整数,但是加一个小数点就会变成浮点数,float f = 1.;这样写则不会报错。
矢量和矩阵GLSL 中还支持矢量和矩阵类型(矢量和矩阵下篇文章将会讲解)。矢量可以理解为一个数组,矩阵可以理解为一个二维数组。
GLSL 中一共有如下类型矢量和矩阵。
vec2,vec3,vec4//分别是包含2,3,4个浮点数的矢量
ivec2,ivec3,ivec4//分别是包含2,3,4个整数的矢量
bvec2,bvec3,bvec4//分别是包含2,3,4个布尔值的矢量
mat2,mat3,mat4//分别是包含2x2,3x3,4x4浮点数元素的矩阵
例如上面例子中我们编写了attribute vec4 a_position这行代码,其中的vec4表示a_position是一个包含 4 个浮点数的变量。
我们给矢量赋值可以直接使用内置的和类型一样的构造函数。如下所示。
vec3pos=vec3(1.,2.,3.0);
浮点数小数点后的 0 是可以省略的,上面写法有点像 JS 中的let pos = [1, 2, 3],与 JS 不同的是 GLSL 中的写法非常灵活。如下所示。
vec3a=vec3(1.);//三个值都被设置为1.,就和vec3(1.,1.,1.)一样
vec2b=vec2(a);//只是用a的前两个浮点数
vec4c=vec4(b,a)//还可以使用多个矢量合成一个矢量,类似与JS中的[...b,...a].slice(0,4)
如果要范围矢量中的值可以使用如下方式。
vec4a=vec4(1.);
a.x//访问第1个值
a.y//访问第2个值
a.z//访问第3个值
a.w//访问第4个值
a.x//访问第1个值
a.y//访问第2个值
a.z//访问第3个值
a.w//访问第4个值
a.x//访问第1个值
a.y//访问第2个值
a.z//访问第3个值
a.w//访问第4个值
a.rgb//混合访问,相当于vec3(1.);
a.xx//重复也可以,相当于vec2(1.);
和矢量一样构造矩阵也是同样使用和类型名字一样的构造函数。
mat4m4=mat4(
1.,2.,3.,4.,
5.,6.,7.,8.,
9.,10.,11.,12.,
13.,14.,15.,16.
);
上面代码定义了一个 4x4 矩阵,类似于在 JS 中这样定义。
letm4=[
[1.,2.,3.,4.],
[5.,6.,7.,8.],
[9.,10.,11.,12.],
[13.,14.,15.,16.]
]
另外 OpenGL 中的矩阵是列主序的,需要转置一下看起来才像一个正常的矩阵(后面文章会详细讲解)。
另外你还可以像下面这样快速创建矩阵。
vec2a=vec2(1.);
vec2b=vec2(1.);
mat2m=mat(a,//矢量也可以作为参数,类似于JS中的[...]数组展开。
访问矩阵中的数值就可以 JS 中访问二维数组的值一样。
vec2a=vec2(1.);
mat2m=mat(a,1.,1.);
a[0][0];//float,第一行第一个元素
a[0];//vec2,第一行
a[0].x;//和矢量一样,还可以使用xyz这些属性。
矢量和矩阵的运算将在后续文章中讲解。
分支和循环GLSL 中的分支和循环和 JS 中一样,如下所示。
if(true){}elseif(true){}else{}
for(inti=0;i3;i++){
continue;//或break
}
唯一的区别是for中的i要指定类型。
函数GLSL 中定义函数语法如下。
返回值类型函数名字(参数类型参数名1,参数类型参数名2,...){
return
}
如果函数没有返回值,函数的返回类型就是void。
floatadd(floata,floatb){
returna+
}
voidmain(){
floatc=add(1.,2.);
}
上面是一个简单的使用函数的例子。和 JS 不同是 GLSL 不允许递归调用,也就是函数不能调用自己。
另外和 c 一样,如果将函数定义在使用函数的下面,需要先声明函数。
floatadd(float,float);
voidmain(){
floatc=add(1.,2.);
}
floatadd(floata,floatb){
returna+
}
函数参数还有 4 个限定词,分别如下。
in向函数输入的值,函数中修改不会影响外部,默认const in向函数输入的值,函数中不能修改out在函数中被赋值,并被传出inout传入函数,并且在函数中可以被修改影响到外面的值。voidadd(floata,floatb,outfloatc){
c=a+
}
voidmain(){
float
add(1.,2.,
}
上面例子中,函数不返回值,而是将结果写入外面传进来的变量。
精度限定字精度限定字用来控制数值的精度,越高的精度也就意味着更慢的性能,所以我们要合理的控制程序的精度。GLSL 中有三种精度highp、mediump和lowp,分别是高、中和低精度。
mediumpfloatsize;//声明一个中精度浮点数
highpint//声明一个高精度整数
lowpvec4//低精度矢量
这样一个一个的变量声明,非常麻烦,我们还可以一次性声明。
precisionmediumpfloat;//浮点数全部使用中精度
GLSL 帮我们设置了一些默认变量精度。顶点着色器中int和float都是highp。
片元着色器中int是mediump,float没有定义。这也就是为什么上面片元着色器中第一行代码是precision mediump float;了,因为 OpenGL 没有设置默认值,所以必须得我们自己设置。
另外sampler2D和samplerCube都是lowp(它们主要用来渲染图片,后面会详细讲解)。
立方体学习了着色器相关知识,现在再来渲染一个 3D 立方体吧。
立方体一共有 6 个面,一个面可以用两个三角形组成,也就是一个立方体需要 12 个三角形,一共需要 36 个顶点!
然而实际上立方体其实只需要 8 个顶点就行了,因为一个顶点可以被多个面复用。在 WebGL 中其实也可以复用顶点,来节省内存。
constgl=createGl()
constprogram=createProgramFromSource(gl,`
attributevec4aPos;
attributevec4aColor;
varyingvec4vColor;
voidmain(){
gl_Position=aPos;
vColor=aColor;
}
`,`
precisionmediumpfloat;
varyingvec4vColor;
voidmain(){
gl_FragColor=vColor;
}
`)
constpoints=newFloat32Array([
-0.5,0.5,-0.5,0.5,0.5,-0.5,0.5,-0.5,-0.5,-0.5,-0.5,-0.5,
0.5,0.5,0.5,-0.5,0.5,0.5,-0.5,-0.5,0.5,0.5,-0.5,0.5
//立方体的8个顶点
])
constcolors=newFloat32Array([
1,0,0,0,1,0,0,0,1,1,0,1,
0,0,0,0,0,0,0,0,0,0,0,0
//每个顶点的颜色
])
constindices=newUint8Array([//面的索引,值是points的下标
0,1,2,0,2,3,//前
1,4,2,4,7,2,//右
4,5,6,4,6,7,//后
5,3,6,5,0,3,//左
0,5,4,0,4,1,//上
7,6,3,7,3,2//下
])
const[posLoc,posBuffer]=createAttrBuffer(gl,program,'aPos',points)
const[colorLoc,colorBuffer]=createAttrBuffer(gl,program,'aColor',colors)
constindexBuffer=gl.createBuffer()
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER,indexBuffer)//绑定buffer到ELEMENT_ARRAY_BUFFER
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER,indices,gl.STATIC_DRAW)
gl.bindBuffer(gl.ARRAY_BUFFER,posBuffer)//绑定buffer到ARRAY_BUFFER
gl.vertexAttribPointer(posLoc,3,gl.FLOAT,false,0,0)
gl.enableVertexAttribArray(posLoc)
gl.bindBuffer(gl.ARRAY_BUFFER,colorBuffer)//绑定buffer到ARRAY_BUFFER
gl.vertexAttribPointer(colorLoc,3,gl.FLOAT,false,0,0)
gl.enableVertexAttribArray(colorLoc)
gl.enable(gl.DEPTH_TEST)
gl.clearColor(0,0,0,0)
gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT)
gl.drawElements(
gl.TRIANGLES,//要渲染的图元类型
indices.length,//要渲染的元素数量
gl.UNSIGNED_BYTE,//元素数组缓冲区中的值的类型
0//元素数组缓冲区中的偏移量,字节单位
)
functioncreateShader(gl,type,source){
constshader=gl.createShader(type)
gl.shaderSource(shader,source)
gl.compileShader(shader)
returnshader;
}
functioncreateProgramFromSource(gl,vertex,fragment){
constvertexShader=createShader(gl,gl.VERTEX_SHADER,vertex)
constfragmentShader=createShader(gl,gl.FRAGMENT_SHADER,fragment)
constprogram=gl.createProgram()
gl.attachShader(program,vertexShader)
gl.attachShader(program,fragmentShader)
gl.linkProgram(program)
gl.useProgram(program)
returnprogram
}
functioncreateAttrBuffer(gl,program,attr,data){
constlocation=gl.getAttribLocation(program,attr)
constbuffer=gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER,buffer)
gl.bufferData(gl.ARRAY_BUFFER,data,gl.STATIC_DRAW)
return[location,buffer]
}
functioncreateGl(width=500,height=500){
constcanvas=document.createElement('canvas')
constgl=canvas.getContext('webgl')
constdpr=window.devicePixelRatio||1
canvas.style.width=`${width}px`
canvas.style.height=`${height}px`
canvas.width=dpr*width
canvas.height=dpr*height
gl.viewport(0,0,canvas.width,canvas.height)
document.body.append(canvas)
returngl
}
效果如下所示。
代码片段
上面例子代码中封装了几个常用函数,来帮助我们加快 WebGL 应用开发,后续的例子中将直接使用这些工具函数,不会再完整的写出来。
要实现复用顶点,需要使用顶点索引,这样 WebGL 就可以通过索引找到对应顶点,因为索引下标都是整数且最大为 7 (因为只有 8 个顶点),所以这里使用了Uint8Array。
然后创建 Buffer 来存放这些索引,和其他attribute数据不同,索引数据的绑定目标是ELEMENT_ARRAY_BUFFER。
上面代码中,我们没有使用drawArrays,而是使用drawElements。 它们的主要区别是drawArrays是根据ARRAY_BUFFER来渲染,drawElements是根据ELEMENT_ARRAY_BUFFER来渲染(根据索引来渲染)。
glDrawArray速度快,费内存(有重复顶点数据)。glDrawElement稍微慢点,省内存(只有一份顶点数据)。
因为我们渲染的是三维物体,需要区分哪个顶点在前哪个顶点在后。上面代码中使用gl.enable(gl.DEPTH_TEST)启用了深度测试,并且使用gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)同时重置颜色缓存和深度缓存。后续文章将会详细讲解这个问题。
上面代码调用了多次bindBuffer方法,可能有同学会觉得已经在createAttrBuffer中调用了一次后面就不用调用了吧。
之前提到过 OpenGL 是一个的状态机,我们在操作 Buffer 之前,需要绑定对应的 Buffer,特别是在代码比较多的情况下,你可能不清楚当前绑定的是哪个 Buffer,直接对 Buffer 进行操作,可能操作到了其他 Buffer。所以在操作 Buffer 之前进行绑定,可以减少 BUG。
varying 存储限定字最后再来看看varying存储限定字,存储限定字其实一共有三个attribute、uniform和varying。上面已经介绍了前两个,它们都是从外部 JS 获取数据。
varying有点特殊,它用于从顶点着色器向片元着色器传送数据。上面例子中我们将aColor赋值给vColor,然后在片段着色器中就可以使用vColor了。 叫varying也是有原因的,我们可以先来看看上面代码最终渲染成什么样子。
顶点着色器是逐顶点的,片段着色器是逐像素的,显然像素会比顶点多。varying变量从顶点着色器向片元着色器传递时会被 OpenGL 插值,也就是我们定义了三角形 3 个顶点的颜色,三角形内部的像素都是根据这 3 个顶点颜色插值出来的。比如一个线段一个端点是红色,另一个是绿色,那么这个线段中间就是 50% 的红色和 50% 的绿色。
总结这篇文章讲解了着色器相关知识,我们可以使用 GLSL 编写顶点着色器和片段着色器来控制物体各个顶点和位置和每个像素的颜色。
由于顶点着色器和片段着色器是在 GPU 中执行,我们需要给他们传递数据。
attribute只能在顶点着色器中使用,可以用它来传递逐顶点的数据,我们会创建一个 Buffer 把数据放入 Buffer 中发送到 GPU,为了提升性能,需要使用Float32Array,这样在 GPU 中就无需再解码数据。
uniform类似全局常量,在顶点着色器和片段着色器中都可以使用,一般会使用它传递一些矩阵,后面文章会有讲解。
varying是用于从顶点着色器向片段着色器传送数据,数据在传递到片段着色器之前会被插值。
在文章的最后我们渲染了一个立方体,但是看起来就像一个正方形,这是因为我们在正前方看这个立方体,为了让它看起来像一个立方体,我们需要让它转动起来,如何转动立方体呢?后续文章将会详细讲解。
如果觉得文章还不错欢迎点赞和关注支持,我会尽快更新系列教程的下一篇文章。
零基础玩转 WebGL 系列文章目录请查看:零基础玩转 WebGL - 目录
阅读原文
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设、网站改版、域名注册、主机空间、手机网站建设、网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。 项目经理在线