正常讲解WebGL的顺序应该是先介绍WebGL所涉及的硬件知识,然后讲解WebGL实际案例,多数的图形学教程一般也是把相关的硬件介绍放在了第一章。 本教程并没有这么做,主要是考虑到术业有专攻,多数学习图形学软件工程师,并不是十分的了解渲染管线和显存的内部数字电路结构。即使放在了第一章, 大家读起来也没什么兴趣,因为行业隔离,可能会导致概念混乱,但是代码是不会骗人的,所以WebGL前几十节课都是以最小的代码案例为中心介绍相关的硬件、WebGL API、着色器语言等知识。 对于零基础的读者,学习前面的课程尽量按照顺序,后面几章关于WebGL图形硬件、渲染管线、WebGL API、着色器语言GLSL ES、数学知识、光照模型等总结性课程可以随时翻看, 一边学习前的代码一边翻看这几章的内容。贫道可以断定,你第一次阅读本章课程和学习完前几十节课的案例代码后阅读本章课程,体会是不同的,尤其是没有接触过图形学又不懂硬件的初学者。 这也是为什么第一节课告诉大家,学习前几十节入门课程的时候,一定要不求甚解,看个大概,甚至不用动手写代码,但是一定要动手改代码、组合代码,做到复制、粘贴、替换就可以。
WebGL图形系统
所谓WebGL图形系统就是一个能够处理WebGL程序并显示的完整计算机系统,就像普通的计算机一样包括输入输出系统、存储器、处理器GPU和CPU。下面的流程图展示的是具有独立显卡普通个人电脑处理WebGL程序的过程。
下面的流程图展示的是具有独立显卡普通个人电脑处理WebGL程序的过程。
执行过程
结合《WebGL绘制一个点》案例讲解,网站上线后,相关的WebGL文件会放在远程的服务器硬盘存储器上,通过网页http请求后, 文件加载到本地硬盘,浏览器解析WebGL程序相关文件,分配内存,存储Javascript程序和数据,CPU会执行Javascript程序,CPU执行着色器初始化函数initShader的时候会把处理编译好的着色器程序发送给GPU处理器, GPU执行着色器程序生成像素数据存入显存,视频控制器会扫描读取显存中的像素数据,显示在浏览器窗口的canvas画布上。
GPU
GPU,全称是图形处理器单元Graphics Processing Unit,GPU是显卡的显示核心,最初的渲染管线不能执行着色器程序,称为固定渲染管线,现在显卡上的GPU渲染管线都能够运行着色器语言,称为可编程渲染管线。 通过GPU渲染管线执行顶点着色器程序和片元着色器程序处理相关数据可以生成像素数据写入显存并显示在屏幕上。
显存
对于拥有独立显卡的电脑,GPU读写数据的存储区就是显卡硬件上的显存。对于没有独立显卡的电脑或手机,显存是GPU和CPU共用的动态内存区域,浏览器解析运行WebGL程序的时候,WebGL图形系统会在内存上开辟一个区域作为显存使用。 显存用来存储GPU渲染管线要处理的顶点数据和生成的片元数据,独立显卡的显存存储结构会针对图形数据进行专门优化,读写数据的效率要比集显或核显效率更高。
帧缓存
帧缓存位于显存上,包含子缓存颜色缓存、深度缓存(Z-缓存)、模板缓存,这些概念里面的缓存也可以称为缓冲区或者说缓冲区对象,比如程序中执行方法createFranebuffer(),会描述为创建一个帧缓冲区对象, 其实就是在显存上开辟一个区域作为帧缓冲区,有独显的帧缓冲区就在显卡上,没有独显的创建帧缓冲区就位于主存上。
通过方法createFranebuffer()创建的自定义帧缓冲区,自定义帧缓冲区的颜色缓冲区的数据不会被视频控制读取显示在canvas画布上,所以称为离屏缓存, WebGL图形系统不需要创建默认存在的一个帧缓冲区,渲染管线生成的像素数据会默认存入到默认帧缓冲区的颜色缓冲区中,系统默认的帧缓冲区称为窗口坐标帧缓冲区或屏幕缓冲区(离屏缓存)。 通过bindFramebuffer()方法可以绑定接收渲染管线片元数据的帧缓冲区,如果不绑定任何离屏缓存,那么渲染管线生成的片元数据就存入屏幕缓存。
颜色缓冲区
颜色缓冲区存储的是像素数据,也就是片元的颜色值,浏览器窗口canvas画布上显示的像素就是颜色缓冲区中的数据。颜色缓冲区存储的像素值可以设置不同的存储格式,比如RGB565就表示三原色红色、绿色、蓝色分量分别占用5、6、5位内存空间,每个像素是16位, 如果RGBA每个分量是8位,也就是一个字节,那么每个像素占用32位内存空间。
深度缓冲区
深度缓冲区又称为Z缓冲区,该缓存中存储的是片元的深度数据,也就是Z坐标轴值,该缓冲区存在的意义就是配合渲染管线上的深度测试单元筛选片元,比如一个不透明的立方体有多个面,每个面都由自己的片元构成,这些片元具有位置和颜色数据,位置包含xy坐标和深度坐标z。 人的眼睛只能看到立方体的正面,也就是说立方体背面片元的颜色值看不到,在canvas画布上同一个坐标位置会有立方体正背面两个片元,通过渲染管线的深度值Z比较大小,舍弃Z值大的片元数据,Z也就是远离眼睛立方体背面的片元。通过测试的片元的颜色值才能够存入颜色缓冲区, 深度缓冲区和颜色缓冲区是对应的,都是某个通过测试的片元的数据。
模板缓冲区
模板缓冲区用来控制颜色缓存某个位置的写入操作,比如绘制物体的阴影就需要用到模板缓冲区。
纹理缓冲区
通过WebGL的相关方法可以把纹理贴图传入显存的纹理缓冲区中,片元着色器可以从纹理缓冲区读取纹理图片像素数据用于实现纹理贴图。 纹理缓冲区还可以作为自定义帧缓冲区的颜色缓冲区,用来接收渲染管线生成的片元颜色数据,渲染管线生成的像素作为纹理图片,实现离屏绘制渲染。
渲染缓冲区
渲染缓冲区可以作为帧缓冲区的子缓冲区使用,用来接收渲染管线生成的像素或深度数据。
顶点缓冲区
通过方法createBuffer()可以在显存上创建一个顶点缓冲区,用来存储顶点的位置、颜色、法向量等数据,渲染管线的顶点着色器可以从顶点缓冲去中获取顶点数据。
渲染管线
渲染管线就像一条流水线,由一系列具有特定功能的数字电路单元组成,下一个功能单元处理上一个功能单元生成的数据,逐级处理数据。顶点着色器和片元着色器是可编程的功能单元,拥有更大的自主性, 还有光栅器、深度测试等不可编程的功能单元。CPU会通过WebGL API和GPU通信,传递着色器程序和数据,GPU执行的着色器程序可以通过useProgram方法切换,传递数据就是把CPU主存中的数据传送到GPU的显存中。
顶点着色器
顶点着色器是GPU渲染管线上一个可以执行着色器语言的功能单元,具体执行的就是顶点着色器程序,WebGL顶点着色器程序在Javascript中以字符串的形式存在,通过编译处理后传递给顶点着色器执行。 顶点着色器主要作用就是执行顶点着色器程序对顶点进行变换计算,比如顶点位置坐标执行进行旋转、平移等矩阵变换,变换后新的顶点坐标然后赋值给内置变量gl_Position,作为顶点着色器的输出,图元装配和光栅化环节的输入。 顶点位置坐标进行矩阵变换的时候需要输入顶点位置坐标数据和顶点执行变换的矩阵数据,顶点位置坐标数据对应的是顶点着色器中关键字attribute声明的变量,顶点变换矩阵对应的是顶点着色器中uniform关键字声明的变量。 和顶点位置数据类似的还有顶点颜色、法向量数据,一个复杂的模型顶点数据的特点是往往是非常多,顶点数据长度不定,和模型复杂度正相关,所以需要专门的顶点缓冲区来缓存, 类似顶点变换矩阵的数据还有光方向、光源位置、光颜色等数据,它们对应的关键字是uniform,这些数据的特点是长度是定值、短小,这些数据存在的意义就是改变顶点位置、颜色数据。
输入
- 使用类型数组创建顶点位置、颜色、法向量、顶点纹理坐标数据,CPU从主存把数据传递给GPU显存上的顶点缓冲区,顶点着色器会在执行程序的过程中逐顶点处理数据
- 几何变换矩阵、光线数据会传递给unifom变量,用来处理改变顶点数据,几何变换矩阵改变的是顶点位置,光线数据改变的是顶点的颜色数据
- 顶点着色器程序
输出
- 需要在光栅化阶段进行插值计算的顶点数据,要赋值给顶点着色器程序中varying关键字声明的变量,作为顶点着色器输出,图元装配、光栅化阶段的输入
- 赋值给内置变量的数据,比如顶点进行矩阵变换后的位置数据赋值给gl_Position作为输出
图元装配
顶点变换后的操作是图元装配(primitive assembly),硬件上具体是怎么回事不用思考,从程序的角度来看,就是绘制函数drawArrays()或drawElements()第一个参数绘制模式mode控制顶点如何装配为图元, gl.LINES的定义的是把两个顶点装配成一个线条图元,gl.TRIANGLES定义的是三个顶点装配为一个三角面图元,gl.POINTS定义的是一个点域图元。线条图元就是两个顶点确定的一条线段,系统默认宽度1个像素, 三角面图元是一个三角形边界线里面所有区域,不是一个三角形的边线,多数图形学书籍描述的是三角形图形,为了避免歧义这里使用三角面一词,同样多数图形学书中把point sprites译为点精灵, 本教程使用了点域描述,图元的点和顶点数据中说的点并不一样,顶点数据中的顶点并没有尺寸,图元中的点通过顶点着色器程序中的内置变量gl_PointSize定义了顶点的像素尺寸,是一个占有一定像素区域的点图元。 这里提到的图元、点精灵等陌生的概念词汇都是来自英文翻译,不必过多关注,为了避免没有学过计算机图形学初学者的概念混乱,专门结合代码进行讲解,而不是空洞的翻译英文概念, 当然最早翻译图形学的先行者的辛劳还是要赞扬传颂的。
光栅化
gl_Position与gl_FragCoord
光栅化就是把图元转化为片元,canvas画布上图像的每一个像素都对应一个片元,片元的尺寸或者说像素尺寸就是显示器的点距参数,一般是10负一次方mm数量级,你可以把片元简单的理解我为像素,事实上两者并不是一个概念, 顶点光栅化得到的原始片元还没有赋予颜色,可以在片元着色器中给片元自定义颜色。仅仅阐述一个片元概念太空洞,以一个立方体为例,它的每个面(图元构成)都会被栅格化为一个个方格片元,这些片元都具有三维空间位置数据。 片元的位置数据来自立方体的顶点插值计算,两个顶点连成一条直线,两点之间片元的坐标都位于该直线上,插值过程是光栅单元自动完成的,除了插值外还要进行坐标变换,顶点的坐标的定义是以世界坐标系作为参照,片元的坐标是以canvas画布窗口坐标系统为参照, 片元坐标在片元着色器程序中使用内置变量gl_FragCoord表示,通过符号.可以访问xyz值,比如gl_FragCoord.xy返回一个vec2类型数据,表示片元在canvas画布上的平面坐标(x,y),xy坐标对应的是片元在屏幕canvas画布上的显示位置, 坐标z表示片元的深度值,以立方体为例,它的每个面都是由光栅化得到的片元组成,通过Z值就可以标识片元距离人眼睛的距离,如果立方体不透明,立方体的背面就看不到,相当于Z值小的片元会覆盖Z值大的片元。 内置变量gl_FragCoord作为光栅化的输出,片元着色器的输入,可以在片元着色器程序中使用。
varying
在顶点着色器程序中赋值给内置变量gl_Position的顶点数据,会被视为顶点坐标,默认插值计算转化为片元的坐标,赋值给varying变量的顶点数据同样会被插值计算,顶点数据可以表示任何你想表示的量,前提是要和顶点位置坐标一一对应, 例如顶点颜色、顶点法向量、顶点纹理坐标,以顶点颜色插值计算为例来说,在顶点着色器程序中把attitude关键字声明的顶点颜色数据赋值给varying关键字声明的变量,光栅化阶段顶点颜色数据会被插值计算,生成的颜色数据和片元一一对应, 有多少个片元,就有多少个与片元对应的RGBA数据。
gl_PointCoord
gl_PointCoord和gl_FragCoord一样也是表示片元坐标的内置变量,区别是gl_FragCoord表示所有片元相对canvas画布这个窗口坐标系统的值,gl_PointCoord表示的是点域图元光栅化后的图元,可以参考本课程第一个WebGL案例, gl_PointSize定义的图元的像素尺寸,gl_PointCoord表示的坐标就是gl_PointSize定义的区域内的片元坐标,点域图元对应片元在canvas画布上的位置根据其对应的顶点坐标变换而来。gl_FragCoord表示的坐标值是绝对的大小,单位是像素, 而gl_PointCoord的大小是相对值,区间是[0,1]。
片元着色器
片元着色器和顶点着色器一样是GPU渲染管线上一个可以执行着色器程序的功能单元,顶点着色器处理的是逐顶点处理顶点数据,片元着色器是逐片元处理片元数据。通过给内置变量gl_FragColor赋值可以给每一个片元进行着色, 值可以是一个确定的RGBA值,可以是一个和片元位置相关的值,也可以是插值后的顶点颜色。除了给片元进行着色之外,通过关键字discard还可以实现哪些片元可以被丢弃,被丢弃的片元不会出现在帧缓冲区,自然不会显示在canvas画布上。
确定颜色值
//红色 gl_FragColor = vec4(1.0,0.0,0.0,1.0);
颜色值关联片元位置
//canvasWidth、canvasHeight表示画布的宽高尺寸 gl_FragColor = vec4(gl_FragCoord.x/canvasWidth,gl_FragCoord.y/canvasHeight,0.0,1.0);
顶点颜色插值
varying vec4 v_Color;//插值后顶点颜色数据
...
gl_FragColor = v_Color;
纹理缓冲区
varying vec2 v_TexCoord;//插值后纹理坐标数据 uniform sampler2D u_Sampler;//插值处理后纹理贴图像素值数据 ... //texture2D方法拾取纹理坐标对应的像素值 gl_FragColor = texture2D(u_Sampler,v_TexCoord);
深度测试单元
深度测试单元位于片元着色器之后,该功能单元不支持可编程,,但是可以通过WebGL方法enable()开启,默认是关闭的。 它的作用就是比较(x,y)坐标相同片元的深度值Z,默认的情况下深度值是gl_FragCoord.z,深度值大表示片元在另一个片元的后面, 深度测试单元会自动抛弃Z值较大的片元,把Z值较小的片元的深度值存入深度缓冲区,像素值RGBA存入颜色缓冲区,同一个(x,y)位置, 复杂场景往往会有多个片元叠加,会多次逐片元比较,不停地更新替换帧缓冲区中已有的深度值和颜色值。
融合单元
融合单元的功能主要是为了实现透明度效果,硬件上能够实现的是把(x,y)坐标相同片元的颜色值进行混合叠加, 和深度测试单元一样可以使用WebGL方法enable()开启。在场景中如果同时存在不透明与透明的物体,一般采取的原则是, 先开启深度测试单元,绘制不透明的几何体,然后设置深度缓冲区为只读模式,然后再绘制透明的几何体。这样的话, 因为深度缓冲区可以读取,位于不透明片元后面的透明片元后被抛弃,由于深度缓冲区已经不可写入, 每一个(x,y)坐标对应的的深度值不会被更小的替换,保证可以进行融合进算,如果不关闭深度缓冲区或者深度检测单元,融合计算就无法进行, 但是绘制透明片元的时候,需要注意片元的叠加关系,默认是按照绘制函数绘制的顺序,这样的话可能片元的前后顺序与实际的情况并不一致, 所以透明片元的排序问题要依赖软件算法实现,硬件只能辅助,颜色融合的渲染一致是一个比较麻烦的问题,关于这方的问题可以搜索一些论文提供的各种算法。