赵工的个人空间


专业技术部分转网页计算转业余爱好部分


  网站建设

首页 > 专业技术 > 网站建设 > WebGL编程
WebGL编程

WebGL是一种3D绘图标准,即Web Graphics Library,是由OpenGL的管理组织Khronos Group基于OpenGL ES2.0所指定的跨平台的Web专用的一种3D绘图标准。WebGL使用canvas来进行显示。

一、WebGL基础:

1. 使用WebGL:

WebGL是在canvas中绘制,如果要使用canvas,与2D图形一样,需要首先在HTML文件中加入<cancas>标记:
<canvas id="mycanvas" width="500" height="500"> 使用的浏览器不支持此功能</canvas>
<canvas>是HTML5标准的内容,一些旧版本的浏览器不支持canvas功能,不支持是会显示出<canvas>和</canvas>标记之间的内容。
然后就需要在<script>脚本中使用JavaScript获取canvas的DOM对象并获取WebGL绘图上下文context:
canvas.getContext(contextType, contextAttributes);
通过 WebGL字符串或experimental-webgl 作为 contentType。contextAttributes参数是可选的,接受布尔值,如下面列出各种选项:

contextAttributes

说明

Alpha

值是 true,它提供了一个alpha缓冲区到画布上。默认是 true

depth

值是true,会得到一个绘图的缓冲区,其中包含至少16位的深度缓冲。默认是true

stencil

值是true,会得到一个绘图的缓冲区,其中包含至少8位的模板缓存。默认值是false

antialias

值是true,会得到一个绘图缓冲区,执行抗锯齿。默认是true

premultipliedAlpha

值是true,会得到一个绘图缓冲区,其中包含的颜色与预乘alpha。默认情况下是true

preserveDrawingBuffer

如果它的值是true,缓冲区将不会被清零,直到被清除或由作者改写将保留它们的值。默认情况下是false

示例:
var canvas = document.getElementById('mycanvas');
var context = canvas.getContext('webgl', { antialias: false, stencil: true });
上述代码创建一个WebGL的上下文模板缓存,将不执行抗锯齿。
因为浏览器厂商及版本不同,contextType常有experimental-webgl和webgl两种形式,为了兼容性,使用如下方法获取context:
canvas = document.getElementById("mycanvas");
gl = getGLContext(canvas);
function getGLContext(canvas) {
  var names = ["webgl", "experimental-webgl"];
  var context = null;
  for (var i=0; i < names.length; i++) {
    try {
      context = canvas.getContext(names[i]);
    } catch(e) {}
    if (context) {
      break;
    }
  }
  if (context) {
    context.viewportWidth = canvas.width;
    context.viewportHeight = canvas.height;
  } else {
    alert("Failed to create WebGL context!");
  }
  return context;
}

代码中将获取WebGL绘图上下文context的过程封装成一个函数来使用,对各个WebGL程序来说都会要用到这个过程,封装成函数使用会方便使用。
目前新版的浏览器基本都支持<cancas>,并且近年版本的大多数浏览器也已支持WebGL三维绘图,但还是有可能遇到不支持此功能的情况,所以函数之中有检测部分。

2. 着色器:

一个典型的WebGL程序的执行流程为:
WebGl 
其中与2D绘图不同的是要使用着色器。WebGL必须使用两种着色器,顶点着色器Vertex shader和片元着色器Fragment shader,其中顶点着色器用来描述顶点特性,如位置、颜色等,片元着色器进行片元处理,如光照等效果,片元fragment可以理解为像素。
WebGL绘图时是将顶点数据放入缓冲区,数据经顶点着色器处理,根据顶点的坐标装配成几何图形,然后经过光栅化处理把几何图形转化成片元,再通过片元着色器获得每个片元的颜色,并写入颜色缓冲区,最后在浏览器中显示出来。
WebGL 

顶点着色器和片元着色器都是使用Open ES着色器语言GLSL ES,这是一种类似C语言的强类型编程语言,其中都包含一个main()函数。JavaScript需要将描述着色器的代码加载编译并传送入WebGL的着色器中,使着色器按代码执行设定的图形绘制。
1)顶点着色器:
顶点着色器控制点的位置和大小,其中需要将绘制图形的位置赋值给gl_Position变量,将点的尺寸赋值给gl_PointSize变量,一般是以矢量的形式赋值。
2)片元着色器:
片元着色器控制点的颜色,需要将点的颜色赋值给gl_FragColor变量,这是片元着色器的唯一内置变量,用来控制像素在屏幕上显示的最终颜色。
着色器是WebGL的核心机制,功能强大而且灵活,但很复杂,是使用WebGL的关键。

3. 着色器变量:

着色器代码中存放着顶点及颜色数据,如果其中都是一些常量,其描述的顶点位置及颜色都只能是固定不变的,而要实现变化的效果,比如将鼠标的位置传入,就需要使用变量。着色器变量有三种,分别是attribute、uniform、varying变量。
attribute变量传输的是那些与顶点相关的数据,只用于顶点着色器中;而uniform变量是所有顶点都相同的数据,或与顶点无关的数据,可用于顶点着色器和片元着色器;varying变量就复杂一些了,要在顶点和片元着色器中进行相同的设置,varying变量的数据在经过顶点着色器时经过了内插操作,然后传输到片元着色器,片元着色器得到的varying变量已经不再是原来的数据了,而是产生了变化,所以称为varying变量,这种变量常用于产生过渡的色彩。示例:
attribute vec4 aVertexColor;
uniform mat4 uMVMatrix;
varying highp vec4 vColor;
所以,在顶点着色器中有可能存在三种变量,而片元着色器中只可能有uniform、varying两种变量,而且如果着色器有varying变量必须两种着色器中都有并且相同。使用片元着色器变量时,需要指定float值的计算精度,如precision mediump float。
每个着色器变量都具有一个存储地址,以便通过存储地址向变量传输数据。如果顶点着色器中有了变量定义:attribute vec4 a_Position;
当需要向着色器变量传输数据时,首先要用JavaScript向WebGL请求该变量的存储地址,对attribute变量和uniform变量的方法有不同:
var a_Position=gl.getAttribLocation(gl.program, 'a_Position');
var u_xformMatrix=gl.getUniformLocation(gl.program,'u_xformMatrix');
然后将改变后的顶点位置传输给相应的变量:
gl.vertexAttrib3f(a_Position, 0.0, 0.0, 0.0);
gl.uniformMatrix4fv(u_xformMatrix,false,xformMatrix);
根据变量的数据格式不同,使用的方法也有不同。

4.着色器代码的加载方式:

顶点着色器和片元着色器都使用了着色器语言GLSL ES,而在Web网页中则是使用JavaScript脚本语言。为了在网页中使用WebGL,就需要使用JavaScript将着色器代码加载进来,一般使用三种方式。
1)赋值给变量:
可以将着色器语言作为字符串赋值给两个JavaScript变量,如:
var VSHADER_SOURCE=
  'attribute vec4 a_Position;\n'
  +'void main() {\n'
  +'  gl_Position=a_Position;\n'
  +'}\n';
var FSHADER_SOURCE=
  'void main() {\n'
  +'  gl_FragColor=vec4(1.0,0.0,0.0,1.0);\n'
  +'}\n';
然后使用loadShader()方法加载并赋值给变量:
var vertexShader = gl.shaderSource(vertexShader, vertexShaderSource);
var fragmentShader = gl.shaderSource(fragmentShader, fragmentShaderSource);
然后进行后续的编译等处理。
2)内嵌为网页脚本:
可以使用<script>标记将着色器语言作为脚本嵌入HTML网页中,但需要设置type属性及id属性,便于JavaScript使用DOM对象获取。示例:
<script id="shader-vs" type="x-shader/x-vertex">
//顶点着色器
attribute vec3 aVertexPosition;
attribute vec4 aVertexColor;
uniform mat4 uMVMatrix;
uniform mat4 uPMatrix;
varying highp vec4 vColor;
void main(void){
 gl_Position=uPMatrix*uMVMatrix*vec4(aVertexPosition,1.0);
 vColor=aVertexColor;
}
</script>
<script id="shader-fs" type="x-shader/x-fragment">
//片元着色器
varying highp vec4 vColor;
void main(void){
 gl_FragColor=vColor;
}
</script>
因为内嵌脚本具有id属性,JavaScript就可以使用document.getElementById()方法获取相应的DOM对象,可以使用DOM对象的innerText或innerHTML属性来获取其中的内容,然后经创建、编译等操作来使用着色器。
3)使用外部着色器文件:
将顶点着色器和片元着色器代码分别存在两个文件中,例如shader.fv、shader.fs,然后使用JavaScript用ajax方式加载进来,然后进行与上面相同的后续处理过程。因为浏览器的安全性要求,加载外部文件必须使用异步的XMLHttpRequest()方法,即ajax方式。
上面三种加入着色器代码的方式中,定义为变量方式最直接,但必须将原代码的每一行都使用引号转为字符串,然后使用“+”连接起来,代码较长时编写不方便,可读性也变差;内嵌方式是将代码整体加入到HTML文件中,编写代码比较方便,但代码可以被作为网页源代码的一部分被看到;使用外部文件加载比较安全,可以保护着色器代码内容,但ajax操作比较繁琐一些。
4)着色器代码加载流程:
因为有三种代码加载方式,加载方式略有不同,但只是前面部分有差别,后面的流程是一致的。如果是赋值给变量的方式,首先根据着色器类型是gl.VERTEX_SHADER或gl.FRAGMENT_SHADER,分别使用gl.createShader()方法创建,然后使用gl.shaderSource()方法指定类型和代码来源,接着使用gl.compileShader()方法分别进行编译。
还要使用gl.createProgram()方法创建一个gl的着色程序对象,继而使用gl.attachShader()方法分别把两种着色器代码载入,然后使用gl.linkProgram()方法链接着色程序对象,再使用gl.useProgram()方法使用着色程序对象。经过这个过程后就可以使用着色器代码了。
因为对不同的WebGL程序,着色器加载流程是一样的,只是载入的代码不同,一般情况下会将着色器加载的程序封装成一个函数来使用,比如initShaders(),可根据三种不同的代码加载方式编写成三种函数。使用变量方式的initShaders()函数代码示例:
function initShaders(gl,vertexShaderSource,fragmentShaderSource){
 var vertexShader = gl.createShader(gl.VERTEX_SHADER);
 gl.shaderSource(vertexShader, vertexShaderSource);
 gl.compileShader(vertexShader);
 var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
 gl.shaderSource(fragmentShader, fragmentShaderSource);
 gl.compileShader(fragmentShader);
 gl.program = gl.createProgram();
 gl.attachShader(gl.program, vertexShader);
 gl.attachShader(gl.program, fragmentShader);
 gl.linkProgram(gl.program);
 if (!gl.getProgramParameter(gl.program, gl.LINK_STATUS)) {
  console.log("Failed to setup shaders");
  return false;
 }
 gl.useProgram(gl.program);
 return gl.program;
}
WebGL依赖着色器,但着色器代码是单独编译的,其中的错误难以被浏览器发现,编程时应加入充分的判断及提示代码部分,便于调试时发现错误的地方及原因,比如代码中的:
if (!gl.getProgramParameter(gl.program, gl.LINK_STATUS)) {
 console.log("Failed to setup shaders"); return false;
}
也可以使用一些WebGL的调试函数库或调试工具,比如webgl-debug.js、blender等。

5. 缓冲区:

缓冲区对象是WebGL系统中的一块存储器,用户可以在缓冲区对象中保存想要绘制的所有顶点的数据,创建缓冲区后就能一次性向顶点着色器中传入多个顶点的attribute变量的数据。使用前一般是先将顶点坐标先赋值给一个JavaScript变量:
var vertices=new Float32Array([0.0, 0.5, -0.5, -0.5, 0.5, -0.5]);
向缓冲区对象写入的数据是一种特殊的JavaScript数组Float32Array。因为attribute变量的数据中可以包含颜色等多种数据,并不能直观看出其中包含多少个顶点,或者有时候只需要绘制部分顶点,一般需要将顶点数量赋值给一个变量:
var n=3;
然后使用gl.createBuffer()方法创建缓冲区对象,再使用gl.bindBuffer()绑定缓冲区对象,这样就可以使用gl.bufferData()方法将顶点数据写入缓冲区。如果要将attribute变量写入缓冲区,要先使用gl.getAttribLocation()获取attribute变量的存储位置,然后使用gl.vertexAttribPointer()将缓冲区对象分配给一个attribute变量,接着使用gl.enableVertexAttribArray()开启attribute变量。上述步骤的示例代码为:
var vertexBuffer=gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
var a_Position=gl.getAttribLocation(gl.program, 'a_Position');
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(a_Position);
也可以将以上代码封装成一个函数,比如initVertexBuffers(),方便在WebGL程序中重复使用。

6. WebGL坐标:

canvas的2D坐标系是以左上角为(0,0),水平方向是x轴,右侧为x轴正方向,垂直方向是y轴,下方是y轴正方向。而WebGL的坐标系则不同,是以canvas中心点为(0,0,0);水平也是x轴,右侧是x轴正方向,左侧则为x轴负方向,即x值为负值;垂直方向为y轴,上方是y轴正方向,下方是y轴负方向,即y值为负值;因为是三维,还有一个z轴,垂直于canvas平面,也即垂直于屏幕,向屏幕外部方向是z轴正方向,而向内为z轴负方向。这种坐标系符合右手规则,是WebGL通常使用的坐标系。
注意canvas坐标与WebGL的差别,并进行相应的坐标转换,在很多情况下很有必要,比如处理对鼠标事件的响应时,就需要将鼠标事件中获取的坐标转化成WebGL中相应的位置。

二、WebGL绘图的基本步骤及主要方法:

1. 一个简单完整的WebGL示例:

了解WebGL的基本流程可以从一个简单的实用程序开始:
var canvas=document.getElementById('webgl');
var gl=getWebGLContext(canvas);
initShaders(gl,VSHADER_SOURCE,FSHADER_SOURCE);
var vertices=new Float32Array([0.0, 0.5, -0.5, -0.5, 0.5, -0.5]);
var n=3;
var vertexBuffer=gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER,vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER,vertices,gl.STATIC_DRAW);
var a_Position=gl.getAttribLocation(gl.program, 'a_Position');
gl.vertexAttribPointer(a_Position, 2,gl.FLOAT,false, 0, 0);
gl.enableVertexAttribArray(a_Position);
gl.clearColor(0.0,0.0,0.0,1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, n);
上述代码会在黑背景中画出一个红色的三角形,其中使用了前面的两个自定义函数getWebGLContext()和initShaders(),而使用的着色器代码是赋值给变量方式的代码。从中可以看出使用WebGL编程的基本流程和基本的一些操作方法。使用WebGL的步骤为:
⑴获取canvas对象
⑵获取WebGL绘图上下文context,这里使用了前面定义的一个兼容不同浏览器的函数
⑶加载着色器代码,这里使用了自定义的加载着色器代码的函数
⑶定义绘制图形的顶点数组
⑷定义绘制图形的顶点数目
⑹创建缓冲区对象gl.createBuffer()
⑺绑定缓冲区对象gl.bindBuffer()
⑻将数据写入缓冲区对象gl.bufferData()
⑼获取attribute变量的存储位置gl.getAttribLocation()
⑽将缓冲区对象分配给一个attribute变量gl.vertexAttribPointer()
⑾开启attribute变量gl.enableVertexAttribArray()
⑿指定绘图区域的背景色gl.clearColor()
⒀清空颜色缓冲区gl.clear()
⒁执行顶点着色器绘制图形gl.drawArrays()

2. 其中使用的主要方法:

1)gl.createBuffer():创建缓冲区对象
方法的返回值为创建的缓冲区对象,创建失败返回null。
2)gl.deleteBuffer(buffer):删除参数指定的缓冲区对象
其中,参数buffer为待删除的缓冲区对象。
3)gl.bindBuffer(target,buffer):允许使用buffer指定的缓冲区对象并将其绑定到目标上
其中,参数target指定了两种缓冲区对象,分别为gl.ARRAY_BUFFER和gl.ELEMENT_ARRAY _BUFFER,buffer指定由gl.createBuffer()创建并返回的待绑定缓冲区对象。
·gl.ARRAY_BUFFER:表示缓冲区对象中包含了顶点的数据
·gl.ELEMENT_ARRAY_BUFFER:表示缓冲区对象中包含了顶点的索引值
4)gl.bufferData(target, data, usage):开辟存储空间并向绑定在target上的缓冲区写入数据
其中,参数target也有gl.ARRAY_BUFFER和gl.ELEMENT_ARRAY _BUFFER两种缓冲区对象;data为写入缓冲区对象的类型化数组数据;usage表示使用存储在缓冲区对象中数据的方式,包括几种值:
·gl.STATIC_DRAW:只会向缓冲区对象中写入一次数据,但需要绘制很多次
·gl.STREAM_DRAW:只会向缓冲区对象写入一次数据,然后绘制若干次
·gl.DYNAMIC_DRAW:会向缓冲区对象中多次写入数据,并绘制很多次
5)gl.clearColor(red, green, blue, alpha):指定绘图区域的背景色
其中,参数red、green、blue、alpha为三基色及透明度值,值的范围为0~1。颜色值越大越亮,透明度值越大越不透明。指定背景色后就会一直存储在WebGL系统中,在下一次调用此方法之前不会改变。
6)gl.clear(buffer):将指定缓冲区设定为预定的值
其中参数buffer用来指定待清空的缓冲区,使用位操作符OR或|可指定多个缓冲区。
·gl.COLOR_BUFFER_BIT:指定颜色缓存
·gl.DEPTH_BUFFER_BIT:指定深度缓冲区
·gl.STENCIL_BUFFER_BIT:指定模板缓冲区
如果buffer参数为gl.COLOR_BUFFER_BIT,清空的是颜色缓冲区,使用gl.clearColor()指定的值。如果没有指定背景色,那么使用的默认值为:

缓冲区

默认值

相关函数

颜色缓冲区

(0.0, 0.0, 0.0, 0.0)

gl.clearColor(red,green,blue,alpha)

深度缓冲区

1.0

gl.clearDepth(depth)

模板缓冲区

0

gl.clearStencil(s)

7)gl.getAttribLocation(program, name):获取由name参数指定的attribute变量的存储位置
其中,参数program指定包含顶点着色器和片元着色器的着色程序对象,是WebGL的上下文gl的program属性,是在初始化着色器时用gl.createProgram()方法创建的;name指定要获取存储地址的attribute变量的名称。返回值为attribute变量的存储位置,一般此地址被赋值给一个JavaScript变量以便以后使用,如果返回-1则表示指定的变量不存在。
8)gl.vertexAttrib3f(location, v0, v1, v2):将数据(v0,v1,v2)传给location参数指定的变量
其中,参数location指定将要修改的attribute变量的存储位置,而v0,v1,v2指定填充attribute变量的三个分量。
location变量是vec4类型,此方法仅传了三个分量,这种情况下默认将第4个分量设置为1.0。其实这个方法包括4种形式,分别为:
gl.vertexAttrib1f(location, v0)
gl.vertexAttrib2f(location, v0, v1)
gl.vertexAttrib3f(location, v0, v1, v2)
gl.vertexAttrib4f(location, v0, v1, v2, v3)
分别是将1个、2个、3个、4个值传入location参数指定的attribute变量。这个方法还包括矢量形式,接受结构化数组作为参数:
gl.vertexAttrib4fv(location, position)
方法名中的4表示attribute矢量中的元素个数为4。
9)gl.drawArrays(mode, first, count):按顶点数据执行着色器
参数mode指定绘制的方式,first指定从哪个顶点开始绘制(整数),count指定绘制要用到点的个数(整数)。
mode参数可接收的常量有:
·gl.POINTS:点
·gl.LINES:线段
·gl.LINE_STRIP:线条
·gl.LINE_LOOP: 回路
·gl.TRIANGLES:三角形
·gl.TRIANGLE_STRIP:三角带
·gl.TRIANGLE_FAN:三角扇
WebGL 
上面的mode参数定义了WebGL绘制的基本图形,其他图形都是使用上述基本图形组合而成,主要是三种三角形方式,使用这些基本图形可绘制出任何物体。
三角形模式有三种,分别是独立的三角形、三角形带、三角扇。使用三角带和三角扇,能绘制的三角形数量是顶点总数减2,而使用独立的三角形则是顶点数量的1/3,所以使用三角带和三角扇需要定义较少的顶点,这样传输的数据量也较少,能提高绘制效率。
为了提高绘制效率,调用gl.drawArrays()方法的次数应越少越好,但遇到很多图形并不能由三角带或三角扇简单组成,存在不连续情况,解决的方法是插入额外的顶点,形成退化三角形。所谓退化三角形,是指至少两个顶点是相同的,面积为0,这种三角形可被系统检测并剔除,但使用退化三角形最好的方法是使用gl.drawElements()方法,顶点数据并没有变化,而只是索引值增加,占用内存更少。
绘制三角形时要注意顶点顺序,绘制立体图形时会使用三角形的顶点顺序来决定三角形的面是否朝向观察者,背向观察者的三角形一般不进行光栅化处理,绘制时会被剔除。WebGL使用下面的指令来实现:
gl.frontFace(gl.CCW);
gl.enable(gl.CULL_FACE);
gl.cullFace(gl.BACK);
上述指令设定逆时针为正面三角形,激活剔除功能,剔除背面三角形。WebGL默认采用逆时针为正面三角形并剔除背面三角形。如果绘制的立体图形是不透明的,一般都剔除背面三角形。
10)gl.drawElements(mode, count, type, offset):指定索引值执行着色器
参数mode指定绘制的模式,同样是7种基本图形;count指定绘制要用到点的个数(整数);type指定索引值数据类型;offset指定以字节为单位的索引数组中开始绘制的位置。
使用这个方法时,将顶点索引写到缓冲区,并绑定到gl.ELEMENT_ARRAY_BUFFER上。使用此方法之前,同样需要先使用gl.bindBuffer()和gl.bufferData()方法,但参数target要使用gl.ELEMENT_ARRAY_BUFFER,而前面不仅要定义顶点坐标数组,还需要定义顶点索引数组。这样在顶点数组并不增加的情况下,只需要在顶点索引数组中增加索引值就可以形成退化三角形。
11)gl.vertexAttribPointer(location, size, type, normalized, stride, offset):
将绑定到gl.ARRAY_BUFFER的缓冲区对象分配给由location指定的attribute变量
其中,参数location指定待分配attribute变量的存储位置;size指定缓冲区中的每个顶点的分量个数,1到4;type用来指定数据格式;normalized传入true或false,表明是否将非浮点数归一化到[0,1]或[-1,1]区间;stride指定相邻两个顶点间的字节数,默认为0;offset指定缓冲区对象中以字节为单位的偏移量,即attribute变量从缓冲区中的何处开始存储,0表示从起始位置开始。type指定的数据格式有:
·gl.UNSIGNED_BYTE:无符号字节Uint8Array
·gl.SHORT:短整型Int16Array
·gl.UNSIGNED_SHORT:无符号短整型Uint8Array
·gl.INT:整型Int32Array
·gl.UNSIGNED_INT:无符号整型Uint32Array
·gl.FLOAT:浮点数Float32Array
12)gl.enableVertexAttribArray(location):开启location指定的attribute变量
其中,参数location指定attribute变量的存储位置。当执行这个方法并传入一个已经分配好缓冲区的attribute变量后,缓冲区对象和attribute变量之间的连接就建立起来了。
但开启attribute变量后,就不能使用gl.vertexAttrib4f()这类方法传数据了,除非显式地关闭该attribute变量。
13)gl.disableVertextAttribArray(location):关闭location指定的attribute变量
其中,参数location指定attribute变量的存储位置。

3. 着色器常用的数据类型:

WebGL专注于绘制三维图形,为了提高绘图三维图形的效率,使用了一些专用的数据类型。
1)vec4:由4个浮点数组成的矢量
内置变量gl_Position和gl_FragColor一般都表示为vec4。gl_Position表示3D的位置坐标,只有3个浮点数,即x、y、z坐标值,但为了进行坐标变换方便,一般加入第4个固定值分量1.0,这样组成的矢量称为齐次坐标。
齐次坐标使用(x,y,z,w)形式,等价于三维坐标(x/w,y/w,z/w),如果齐次坐标的第4个分量是1就可以当作三维坐标来使用。w的值必须是大于等于0的,如果w趋近于0,所表示的点将趋近无穷远。
2)类型化数组:
为了绘制三维图形,WebGL通常需要同时处理大量相同类型的数据,为了优化性能,WebGL引入了特殊的一种数组,称为类型化数组。WebGL常用的类型化数组有:

数组类型

每个元素所占字节数

对应的C语言中的数据类型

Int8Array

1

8位整型数(signed char)

UInt8Array

1

8位无符号整型数(unsigned char)

Int16Array

2

16位整型数(signed short)

UInt16Array

2

16位无符号整型数(unsigned short)

Int32Array

4

32位整型数(signed int)

UInt32Array

4

32位无符号整型数(unsigned int)

Float32Array

4

单精度32位浮点数(float)

Float64Array

8

双精度64位浮点数(double)

类型化数据有一系列属性和方法,但类型化数组不支持push()和pop()方法。

常量、属性和方法

描述

BYTES_PER_ELEMENT

数组中每个元素所占的字节数

length

数组的长度

get(index)

获取第index个元素的值

set(index, value)

设置第index个元素的值为value

set(array, offset)

从第offset个元素开始将数组array中的值填充进去

创建类型化数组的唯一方法是通过new运算符调用构造函数并传入数据。示例:
var vertices=new Float32Array([0.0, 0.5, -0.5, -0.5, 0.5, -0.5 ]);
也可以通过指定数组元素个数来创建一个空的类型化数组:
var vertices=new Float32Array(4);

4. 加载着色器代码使用的方法:

WebGL编译GLSL ES代码并创建和初始化着色器要使多个方法:
1)gl.createShader(type):创建由type指定的着色器对象
其中,参数type指定创建着色器的类型,gl.VERTEX_SHADER表示顶点着色器,gl.FRAGMENT _SHADER表示片元着色器。创建成功返回创建的着色器对象,失败返回null。
2)gl.deleteShader(shader):删除指定的着色器对象
其中,参数shader为待删除的着色器对象。如果着色器对象还在使用,并不会立刻删除着色器,要等到程序不再使用该着色器后才删除。
3)gl.shaderSource(shader, source):将source指定的字符串形式代码传入指定的着色器
其中,参数source指定字符串形式的GLSL ES源代码,参数shader指定需要传入代码的着色器对象。如果之前已经向shader传入过代码了,旧的代码将会被替换掉。
4)gl.compileShader(shader):编译shader指定的着色器中的源代码
其中,参数shader为指定的有源代码的着色器。GLSL ES语言的着色器源代码需要编译成二进制格式才能被WebGL使用。通过gl.shaderSource()用新代码替换掉旧代码后也需要重新编译。
5)gl.getShaderParameter(shader, pname):获取指定着色器中pname指定的参数状态
其中,参数shader指定待获取参数的着色器,pname指定待获取参数的类型。编译时,如果出现错误,可以调用此方法来检查着色器的状态。
pname参数的类型有:

参数类型

描述

返回值

gl.SHADER_TYPE

返回是顶点着色器还是片元着色器

gl.VERTEX_SHADER或gl.FRAGMENT_SHADER

gl.DELETE_STATUS

返回着色器是否被删除成功

true或false

gl.COMPILE_STATUS

返回着色器是否被编译成功

true或false

6)gl.getShaderInfoLog(shader):获取shader指定的着色器的信息日志
其中,参数shader指定获取信息日志的着色器。如果编译错误,WebGL会把错误内容写入着色器的信息日志Information log,可以通过此方法来获取其信息字符串,具体格式依赖于浏览器。如果没有错误返回null。
7)gl.createProgram():创建程序对象
此方法没有参数,返回值为创建的程序对象,创建失败返回null。程序对象包含了顶点着色器和片元着色器,获取着色器中的全局变量位置时也需要指定这个程序对象。
8)gl.deleteProgram(program):删除指定的程序对象
其中,参数program指定待删除的程序对象。如果该程序对象正在被使用不立刻删除,而是等它不再被使用后再删除。
9)gl.attachShader(program, shader):将shader指定的着色器分配给指定的程序对象
其中,参数program指定程序对象,参数shader指定着色器对象。着色器在附给程序对象之前并不一定要为其指定代码或进行编译,空的着色器也可以附给程序对象。
10)gl.detachShader(program, shader):取消shader指定的着色器对指定程序对象的分配
其中,参数program指定程序对象,参数shader指定着色器对象。
11)gl.linkProgram(program):连接program指定的程序对象中的着色器
其中,参数program指定程序对象。程序对象进行着色器连接操作,可以保证顶点着色器和片元着色器的varying变量同名同类型,且一一对应;保证顶点着色器对每个varying变量已赋值;保证顶点着色器和片元着色器中同名的uniform变量也是同类型的;保证着色器中的attribute变量、uniform变量和varying变量的个数没有超过着色器的限制。
12)gl.getProgramParameter(program, pname):获取指定程序对象中pname指定的信息
其中,参数program指定程序对象,参数pname指定待获取参数的类型。
pname可指定的参数类型有:

参数类型

描述

返回值

gl.DELETE_STATUS

程序是否已被删除

true或false

gl.LINK_STATUS

程序是否已经成功删除

true或false

gl.VALIDATE_STATUS

程序是否已经通过验证

true或false

gl.ATTACHED_SHADERS

已被分配给程序的着色器数量

数值

gl.ACTIVE_ATTRIBUTES

顶点着色器中attribute变量的数量

数值

gl.ACTIVE_UNIFORMS

程序中uniform变量的数量

数值

13)gl.getProgramInfoLog(program):获取program指定的程序对象的信息日志
其中,参数program指定待获取信息日志的程序对象。如果程序对象连接失败,可以使用此方法获取连接出错信息,返回值为包含日志信息的字符串。
14)gl.useProgram(program):告知WebGL系统绘制时使用指定的程序对象
其中,参数指定待使用的程序对象。WebGL中,可以准备多个程序对象,在绘图时根据需要切换程序对象。

三、基本数学运算和变换:

绘制3D图形涉及很多数学知识,主要是坐标、向量、矩阵及其运算。

1. 坐标:

三维图形有三个坐标轴,通常用x、y、z来表示,三个坐标轴中的任何一个都与另外两个轴垂直,而一般使用右手坐标系。所谓右手坐标系,是指x、y、z轴的正方向分别对应于右手的拇指、食指和中指。
而3D坐标系中的任何一个点的位置,都使用有序的三个值(x,y,z)来表示。两个点可以定义一个线段,三个点可以定义一个三角形,这时的三个点称为三角形的顶点。
在3D空间,用三个坐标指定一个点或一个矢量,不好分清是点还是矢量,一般需要加上第4个坐标w。当w=0为矢量,当w≠0时指定一个点。这种坐标称为齐次坐标。当使用齐次坐标时,(x, y, z, w)与(x/w, y/w, z/w, 1)是等效的,也就是3D空间中的点,第4个分量设为1,就得到一个点的齐次坐标。
使用齐次坐标,可以使用4x4的矩阵来进行坐标变换,执行平移、旋转、缩放、剪切等操作,计算起来比较方便。

2. 矢量:

与空间方向无关的物理量称为标量,而与空间的方向有关的量则称为矢量。矢量是两个点的差,有方向和长度。3D空间中,矢量用三个坐标来表示,称为矢量的三个分量。WebGL中,矢量用来指定光线的方法和视线的方向,还会使用法向矢量。一个平面的法向垂直于这个平面,一般用于确定平面的朝向。游戏中的物体速度、加速度等也是矢量。矢量可以进行一些基本运算。
1)矢量相加:
两个矢量相加,即把两个矢量的各个分量进行相加。
2)矢量乘以标量:
矢量乘以一个标量,是矢量的各个分量都乘以这个标量值。如果乘以标量-1,就得到一个与原矢量大小相等而方向相反的矢量。
3)矢量的点积:
两个矢量相乘,有点积scalar product和叉积cross product两种方式。两个矢量u和v的点积定义为:
u·v=|u||v|cosθ
点积的结果是个标量,其值为两个矢量的长度值相乘并乘以它们之间的夹角的余弦值。如果两个矢量正交,即互相垂直,点积结果为0。
点积也可以使用分量形式来表示:
WebGL
WebGL中使用矢量的点积计算物体表面的反射光。
4)矢量的叉积:
两个矢量的叉积用分量表示为:
 WebGL
叉积的结果是一个矢量,其值为两个矢量的长度值相乘并乘以它们之间的夹角的正弦值,而方向则垂直与两个矢量组成的平面且符合右手定则。也就是叉积的值为|w|=|u||v|sinθ,而得到的新矢量的方向与两个矢量的顺序有关,顺序相反得到的方向也相反。
在WebGL中,叉积常用于计算三角形表面的法向矢量,常用于计算光照效果及判断正向三角形。

3. 矩阵:

矩阵是由行和列组成,其中的每个元素称为矩阵的元素。一个由m行和n列组成的矩阵,称为维数为mxn。
WebGL中,最常用的矩阵是4行4列的矩阵:
WebGL
只有一列的矩阵称为列矢量,即大小为mx1,4个元素的列矢量表示为:
WebGL
只有一行的矩阵称为行矢量,即矩阵维数为1xn,4个元素的行矢量表示为:
WebGL
在WebGL中,列矢量很常见,一列表示一个顶点。列矢量乘以一个4x4矩阵表示对这个顶点执行某种变换。矩阵变换的计算往往比较繁琐复杂,一般编程时会使用JavaScript函数库,最常用的是glMatrix,其主要就是为WebGL设计的,支持3x3、4x4矩阵的运算。
1)矩阵的加减:
只有维数完全相同的两个矩阵才可以相加减。两个矩阵的相加运算是对每个元素相加,相减运算是对每个元素相减。
2)矩阵相乘:
矩阵相乘是3D中非常重要的运算。只有当矩阵A的列数等于矩阵B的行数时,矩阵A才可以乘以矩阵B。如果矩阵A的行数为m列数为p,即维数mxp;而矩阵B的行数为p列数为n,即维数为pxn;则矩阵A乘以矩阵B得到一个m行n列的矩阵。新矩阵AB在ij位置的元素是矩阵A的第i行与矩阵B的第j列的标积,即:
WebGL
矩阵运算的顺序非常重要,不符合交换律。在WebGL中最常用的矩阵相乘是两个4x4矩阵相乘,或一个4x4矩阵与一个4x1矩阵相乘运算。
3)单位矩阵和逆矩阵:
对于标量,任何数x乘以1结果不变。对应的矩阵是单位矩阵,一个单位矩阵是一个方阵,它的行数与列数相等,而且对角位置的元素都为1,其他位置的元素都为0:
WebGL
一个矩阵M与单位矩阵I相乘,结果还是矩阵M,即:
WebGL
对于标量,除0之外,存在一个倒数,与倒数相乘的结果为1。对一个矩阵,有时候也存在逆矩阵,两个矩阵相乘的结果是单位矩阵:
WebGL
只有方阵才有逆矩阵,但并非所有方阵都有逆矩阵,只有其行列式|M|的值不为零的方阵才有逆矩阵。
4)转置矩阵:
矩阵M转置后得到另一个矩阵,它的行是M的列,它的列是M的行。对于任何mxn矩阵都有转置矩阵。

4. 仿射变换:

在屏幕上显示3D图形时,经常需要用到变换,比如转换坐标系,进行平移、旋转、缩放、剪切等。3D空间的点或矢量,如果采用4个分量的齐次坐标表示,就可以把一个4x4的特定矩阵乘以列矢量来实现上述变换,这些变换都属于为仿射变换。
1)平移:
平移是指一个物体的每个顶点都移动一个相同的位移。平移矩阵可以表示为:
WebGL
对一个点进行平移变换时,位移由矢量(tx,ty,tz)表示。使用平移矩阵与坐标点的齐次坐标相乘,结果就是此点沿矢量(tx,ty,tz)平移后的位置的齐次坐标。
WebGL
对于矢量,只有方向和大小,而没有位置,齐次坐标的第4个分量为0。平移矩阵乘以矢量的齐次坐标,矢量的齐次坐标保持不变。
2)旋转:
旋转是指一个物体以经过坐标原点的直线为轴旋转一个角度。旋转矩阵通常用Rx、Ry、Rz来表示,分别表示绕x轴、y轴、z轴旋转。旋转矩阵分别表示为:
WebGL WebGL WebGL
一个点的旋转,可以使用相应的旋转矩阵乘以此点的齐次坐标,得到的就是旋转后的点的齐次坐标。
3)缩放:
缩放是用来放大或缩小一个物体。缩放矩阵表示为:
WebGL
缩放矩阵作用于某个物体,使其x方向缩放sx倍、y方向缩放sy倍、z方向缩放sz倍。如果sx=sy=sz就是均匀缩放,均匀缩放会改变物体的大小但不会改变其形状。
4)剪切:
把单位矩阵左上角3x3子阵中的某个0改为非零值,就可以得到剪切矩阵。单位矩阵左上角3x3子阵中有6个0,所以在3D空间有6个基本的剪切矩阵,常命名为Hxy(s)、Hxz(s)、Hyx(s)、Hyz(s)、Hzx(s)、Hzy(s),其中第1个下标表示执行此剪切的坐标。Hxy(s)矩阵为:
WebGL
这个剪切矩阵用y坐标取改变x坐标。将此剪切矩阵作用于一个点时:
WebGL
从中可以看出,当y值增加时,x坐标轴向右切变。剪切在WebGL中很少用到,游戏中需要扭曲场景时会使用。

5. glMatrix库:

目前比较常用的用于矢量计算和矩阵变换的开源的JavaScript库的是glMatrix,其主要就是为WebGL设计的,支持3x3、4x4矩阵的运算。
glMatrix库中大多数执行向量和矩阵运算的函数都有一个参数表示此运算针对向量还是矩阵。作用于向量的函数采用的形式为:
vac3.opeation(srcVec, otherOperands, descVec(optional));
如果指定了descVec参数,则运算结果写入descVec中,并将其作为函数值返回;如果没有指定descVec参数,则运算结果写入srcVec中并返回,即其中的内容会被修改。
1)向量的创建:
创建一个3元素向量使用下面的方式:
vec3.creat(vec)
这里vec是一个可选的数组,包含3个初始元素。示例代码:
var u=vec3.create([1,2,3]);
也可以不用初值直接创建一个3分量向量:
var u=vec3.create();
2)向量加法:
定义两个向量u和v,然后将两个相加,结果存入向量s:
var u=vec3.create([1,2,3]);
var v=vec3.create([4,5,6]);
var s=vec3.create();
vec3.add(u, v, s);
3)其他向量运算:
var c=vec3.dot(u, v, c); //向量的点积
var d=vec3.cross(u, v, d); //向量的叉积
4)创建矩阵:
创建一个4x4矩阵使用以下方法:
mat4.create(mat)
其中,mat是一个可选数组,包含了这个矩阵的16个初始元素。矩阵中,元素使用列主序,即前4个元素对应矩阵的第1列,接下来的4个元素对应于矩阵的第2列,以此类推。
指定初始数组时的示例为:
var N=mat4.create([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5]);
5)矩阵乘法:
计算矩阵M和N的乘积MN使用的方法为:
var MN=mat4.multiply(M, N);
乘法运算中没有指定目标矩阵,相乘结果存入M矩阵中。
6)矩阵变换:
mat4.translate(I,[2,3,4],T); //平移矩阵
mat4.rotate(I,[2,3,4],T); //旋转矩阵
mat4.scale(I,[2,3,4],T); //缩放矩阵

四、WebGL的矩阵变换:

WebGL编程中经常会用到矢量、矩阵,对物体平移、旋转、缩放等常用操作也常会用到矩阵变换等运算。

1. WebGL中常见的变换:

在WebGL中经常使用到模型变换、视图变换、模型视图变换、投影变换、视口变换等。
1)模型变换:
3D场景中的每个物体都有自身的坐标系,要使用模型变换来确定其在全局坐标系统中的位置和方向,模型变换通常由平移、旋转和缩放组成。
只需要一个4x4矩阵就可以保存平移、旋转和缩放的全部组合,将物体模型的顶点从本身的坐标系变换到世界坐标系只需要将顶点坐标与这个4x4矩阵相乘,在顶点着色器中很容易实现矩阵相乘运算。
2)视图变换:
3D场景中,WebGL只对照相机(即观察者)能够看到的对象进行绘制,视图变换用来确定物体在照相机中的位置和方向。照相机总是位于原点位置,而且视线朝向z轴负方向。视图变换的作用实际上是设置一个矩阵,对场景中顶点的变换效果与变换照相机的效果相反。
视图变换通常由平移和旋转组成。顶点经过视图变换后,就处于眼睛坐标系中。视图变换也可以使用简单的mat4.lookAt()方法,这个函数根据传入的照相机位置、视线方向和一个用来定义向上方向的向量来创建一个视图变换矩阵。
3)模型视图变换:
在处理场景在屏幕上的显示时,模型变换和视图变换实际上是一回事,重要的是场景中照相机与对象之间的关系。模型视图变换指将模型变换和视图变换合并成一个变换矩阵,把顶点与合并后的模型视图矩阵相乘,就将顶点直接从自身的坐标系转换为眼睛坐标系。
4)投影变换:
投影变换确定如何将3D场景投影到屏幕上,也决定了视域的外观。投影变换实际上是将视域变换成一个单位立方体,这个立方体在3个坐标轴上的取值范围都是-1~1。
顶点着色器中的位置变量gl_Position要处在视域单位立方体中,位于此外的顶点和图元都会被裁剪掉。为了使物体能在视域中显示出来,就需要利用投影矩阵对物体的顶点坐标进行变换,使变换后的顶点坐标在写入gl_Position变量之前都位于视域单位立方体内。投影变换有两种,正交投影和透视投影。
正交投影:
也称为平行投影,平行线经过投影后仍然平行,场景中的物体经过投影后保持它们之间的相对大小,这种投影常用于CAD软件中。正交投影的视域是一个矩形,用glMatrix库中的mat4.ortho()函数可以设置正交投影矩阵:
mat4.ortho(left, right, bottom, top, near, far, projecttionMatrix);
此函数创建一个投影矩阵,相当于一个长方体视域,其左边界由参数left确定,右边界由right参数决定,底边界由bottom参数决定,顶边界由top参数决定,近平面由near参数决定,远平面由far参数决定。观察者在这个长方体的正前方的无限远处。
透视投影:
当使用透视投影时,离观察者较远的物体看起来比较小,而离观察者较近的物体则看起来比较大,这种投影接近于人眼的视觉,可以提供比较真实的场景。透视投影的视域是一个锥台,也称为视锥体,相当于去掉顶部的金字塔形。
glMatrix中有两个函数用来设置透视投影矩阵:
·mat4.perspective()
·mat4.frustum()
mat4.perspective()函数的原型为:
mat4.perspective(fvoy, aspect, near, far, projecttionMatrix);
其中,参数fvoy表示视域的垂直范围,ascpect是纵横比(视口的宽与高的比值),near是视域的近平面离观察者的距离,参数far是视域的远平面离观察者的距离。调用此方法得到一个投影矩阵。
mat4.frustum()也可以用来定义一个透视投影矩阵,函数原型为:
mat4.frustum(left, right, bottom, top, near, far, projecttionMatrix);
其中,参数left确定近平面的左边界,参数right决定近平面的右边界,参数bottom决定近平面的底边界,参数top决定近平面的顶边界,参数near决定近平面的位置,参数far决定远平面的位置。
5)透视除法:
当顶点着色器将坐标写入gl_Position变量时,在剪裁坐标系中进行操作,坐标使用包含4个分量的齐次坐标(x, y, z, w)来表示。在图元装配时,顶点要经过透视除法,即将所有的坐标除以w,得到归一化的坐标(x/w, y/w, z/w, 1)。不过,这一过程是系统处理的,用户不用处理。
6)视口变换:
视口变换是图元装配操作的一部分,但用户可以通过下面的方法影响视口变换:
gl.viewPort(x, y, w, h);  //使用视口左下角坐标(x, y)、宽度w和高度h定义一个视口。
gl.depthRange(n, f);   //用来定义用户希望达到的景深范围,n代表近平面,f代表远平面
gl.depthRange()方法的默认值为gl.depthRange(0.0, 1.0)。
从归一化物体坐标(u, v, w)变换为窗口坐标(m, n, p)要使用下面的公式:
WebGL
其中,w和h代表视口的宽度和高度,单位为像素,用于gl.viewPort()方法中。而Ox和Oy代表视口的中心坐标,Ox=(x+w)/2和Oy=(y+h)/2。

2. WebGL变换流程:

WebGL是完全基于着色器的,用户可以控制顶点着色器中的变换矩阵。定义着色器时需要使用attribute变量存放顶点数据,而使用uniform变量存放变换矩阵,还要在顶点着色器的main()函数中定义顶点数据与变换矩阵的算法公式。JavaScript创建并绑定缓冲区,将顶点数据存放到WebGL的Buffer中,顶点着色器就能从Buffer中读取数据,从attribute变量获取到顶点数据,从uniform变量获取到变换矩阵,然后按照预定义的算法进行模型视图变换,使顶点处于眼睛坐标系中;如果需要在顶点着色器中执行光照效果,通常是在眼睛坐标中进行,也要使用通过uniform变量传入的变换矩阵。然后将顶点坐标乘上投影矩阵,把得到的顶点坐标写入gl_Position变量中,系统经过透视除法得到归一化的设备坐标。最后归一化坐标通过视口变换映射到实际屏幕坐标。

3. WebGL变换中常用的方法:

因为平移、旋转、缩放等操作都是针对物体的所有顶点进行的操作,所以操作的变量要使用顶点着色器中的uniform变量。常用的对uniform变量的操作方法有几种:
1)gl.getUniformLocation(program, name):获取uniform变量的存储位置
其中,参数program指定包含顶点着色器和片元着色器的着色程序对象,name指定要获取存储地址的uniform变量的名称。返回值为uniform变量的存储位置,一般此地址被赋值给一个JavaScript变量以便以后使用,如果返回null则表示指定的变量不存在。
2)gl.uniformMatrix4fv(location, transpose, array):
将array表示的4x4矩阵分配给location指定的uniform变量
其中,参数location表示uniform变量的存储位置;参数transpose表示是否转置矩阵,因为在WebGL中没有提供矩阵转置的方法,所以必须指定为false;array为待传输的类型化数组,4x4矩阵按列主序存储在其中。
3)gl.uniform4f(location, v0, v1, v2,v3):将数据(v0,v1,v2,v3)传给location参数指定的变量
其中,参数location指定将要修改的uniform变量的存储位置,而v0,v1,v2,v3指定填充uniform变量的4个分量。
此方法也包括多种形式,分别为:
gl.uniform1f(location, v0)
gl.uniform2f(location, v0, v1)
gl.uniform3f(location, v0, v1, v2)
gl.uniform4f(location, v0, v1, v2,v3)

4. 使用矩阵变换的示例:

顶点着色器的代码为:
var VSHADER_SOURCE=
 'precision mediump float;\n'
  +'attribute vec4 a_Position;\n'
  +'uniform mat4 u_xformMatrix;\n'
  +'void main() {\n'
  +'  gl_Position=u_xformMatrix*a_Position;\n'
  +'}\n';
使用uniform变量传输旋转矩阵的部分代码:
var canvas=document.getElementById('webgl');
var gl=getWebGLContext(canvas);
initShaders(gl,VSHADER_SOURCE,FSHADER_SOURCE);
var vertices=new Float32Array([0.0, 0.5, -0.5, -0.5, 0.5, -0.5]);
//设置顶点位置
var n=initVertexBuffers(gl);
//创建旋转矩阵
var ANGLE=90.0;
var radian=Math.PI*ANGLE/180.0; //角度转弧度
var cosB=Math.cos(radian);
var sinB=Math.sin(radian);
var xformMatrix=new Float32Array([
  cosB, sinB, 0.0, 0.0,
  -sinB, cosB, 0.0, 0.0,
  0.0, 0.0, 1.0, 0.0,
  0.0, 0.0, 0.0, 1.0
]);
//将旋转矩阵传输给顶点着色器
var u_xformMatrix=gl.getUniformLocation(gl.program,'u_xformMatrix');
gl.uniformMatrix4fv(u_xformMatrix,false,xformMatrix);
gl.clearColor(0.0,0.0,0.0,1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, n);
代码只是旋转了一个三角形,比较简单。其中顶点着色器中加入了uniform变量,使用了mat4的矩阵数据类型,后面的main()函数中定义了矩阵与顶点的attribute变量的算法。后面的JavaScript代码中,首先使用了自定义的initVertexBuffers()函数创建并绑定缓冲区,还包括将数据写入缓冲区及传入顶点着色器的attribute变量,后面的代码中计算了旋转角度的三角函数值,然后组成Float32Array格式的旋转矩阵,然后使用gl.getUniformLocation()方法获取uniform变量在buffer中的存储位置,再使用gl.uniformMatrix4fv()方法将旋转矩阵传入顶点着色器。最后一部分还是设置背景色、清空颜色缓冲区并绘制图形。上述代码并没有使用glMatrix函数库,顶点数据与旋转矩阵的乘法是着色器计算的。

五、色彩和纹理:

1. 色彩:

WebGL要产生色彩要使用片元着色器。如果只是单一颜色,片元着色器中的内置变量gl_FragColor可以设置为固定值:
gl_FragColor=vec4(1.0,0.0,0.0,1.0);
但大多数情况下,同一个平面上要产生复杂的色彩,这时候就需要片元着色器要使用变量。
1)产生与坐标相关的色彩:
其中的着色器代码为:
var VSHADER_SOURCE=
 'attribute vec4 a_Position;\n'
 +'void main() {\n'
 +'  gl_Position=a_Position;\n'
 +'}\n';
var FSHADER_SOURCE=
 'precision mediump float;\n'
 +'uniform float u_Width;\n'
 +'uniform float u_Height;\n'
 +'void main() {\n'
 +'  gl_FragColor=vec4(gl_FragCoord.x/u_Width, 0.0, gl_FragCoord.y/u_Height, 1.0);\n'
 +'}\n';
片元着色器内置颜色变量gl_FragColor中的红色和蓝色分量被赋值为与片元的坐标gl_FragCoord有关。其中gl_FragCoord的x坐标影响红色分量,y坐标影响蓝色分量。因为gl_FragColor变量的每个分量都是0~1之间的值,所以要使用其坐标点与绘图区域的宽度和高度的比值。
canvas的宽度和高度值需要使用uniform变量传入片元着色器中。要在片元着色器中使用uniform变量,需要使用gl.getUniformLocation()方法获取uniform变量在缓冲区中的存储位置,然后使用gl.uniform1f()方法将绘图区的宽度和高度传入其中。示例代码为:
var vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
var u_Width = gl.getUniformLocation(gl.program, 'u_Width');
var u_Height = gl.getUniformLocation(gl.program, 'u_Height');
gl.uniform1f(u_Width, gl.drawingBufferWidth);
gl.uniform1f(u_Height, gl.drawingBufferHeight);
gl.enableVertexAttribArray(a_Position);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
然后就可以使用gl.drawArrays()方法画出与坐标位置有关色彩变换了。
2)使用varying变量产生变化的色彩:
为了产生复杂的色彩效果,一般要在着色器中使用varying变量。varying变量要在顶点着色器和片元着色器中进行相同的定义,数据经过顶点着色器时经过了内插操作,然后传输到片元着色器以产生色彩。一个简单的着色器代码为:
var VSHADER_SOURCE=
 'attribute vec4 a_Position;\n'
 +'attribute vec4 a_Color;\n'
 +'varying vec4 v_Color;\n'
 +'void main() {\n'
 +'  gl_Position=a_Position;\n'
 +'  v_Color=a_Color;\n'
 +'}\n';
var FSHADER_SOURCE=
 'precision mediump float;\n'
 +'varying vec4 v_Color;\n'
 +'void main() {\n'
 +'  gl_FragColor=v_Color;\n'
 +'}\n';
着色器代码中,顶点着色器中先定义了一个attribute变量a_Color,然后又定义了varying变量v_Color,在main()函数中定义v_Color颜色变量来自于传入的与顶点有关的颜色a_Color。在片元着色器中,也同样定义了varying变量v_Color,片元的内置变量gl_FragColor来自于v_Color,也就是颜色值来自于顶点着色器中定义的颜色a_Color。但是片元着色器中得到的颜色值已经不是a_Color原值,而是在光栅化时经过了插值,每个片元因此得到了内插后产生的有渐变效果的颜色。处理varying变量的代码为:
var verticesColors=new Float32Array([0.0, 0.5, 1.0, 0.0, 0.0, -0.5, -0.5, 0.0, 1.0, 0.0, 0.5, -0.5, 0.0, 0.0, 1.0]);
var vertexColorBuffer=gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER,vertexColorBuffer);
gl.bufferData(gl.ARRAY_BUFFER,verticesColors,gl.STATIC_DRAW);
var FSIZE=verticesColors.BYTES_PER_ELEMENT;
var a_Position=gl.getAttribLocation(gl.program, 'a_Position');
var a_Color=gl.getAttribLocation(gl.program, 'a_Color');
gl.vertexAttribPointer(a_Position, 2,gl.FLOAT,false, FSIZE*5, 0);
gl.vertexAttribPointer(a_Color, 3,gl.FLOAT,false, FSIZE*5, FSIZE*2);
gl.enableVertexAttribArray(a_Position);
gl.enableVertexAttribArray(a_Color);
代码中的顶点数组中有顶点坐标值及颜色值,后面使用BYTES_PER_ELEMENT属性获取每个元素的字节值,以便在使用gl.vertexAttribPointer()方法时能指定对应的位置。使用varying变量时只需要传入相应的顶点坐标和颜色的attribute变量,varying变量是系统内部处理的。

2. 纹理:

为了产生接近真实的物体表面效果,通过增加更多三角形以产生复杂的表面并产生更多片元颜色会变得非常繁琐复杂,系统运行能力也会成为限制。为了解决这种实际需要,就研究出了纹理映射texture mapping,即将一张图像贴到一个几何图形上,可以使用PNG、JPG或GIF格式图像。
WebGL还可以使用canvas元素、视频元素、ImageData对象和类型化数组中的原始数据作为2D纹理的输入数据。WebGL还支持立方映射纹理,一个立方映射纹理由6个正方体纹理组成,常用于环境映射,即在细小对象上产生环境反射效果。
使用的纹理图像,经常选用宽度和高度都是2的n次方的图像,这是因为老式GPU只支持纹理的宽度和高度都是2的n次方。OpenGL ES 2.0和WebGL中允许使用非2的n次方(NPOT)的纹理,但这时不能使用Mip映射贴图,而重复模式则只能用gl.CLAMP_TO_EDGE。
1)纹理映射的步骤:
纹理映射,就是根据纹理图像,为光栅化后的每个片元涂上合适的颜色。WebGL中,进行纹理映射的步骤为:
·准备映射到几何图形的纹理图像
·为几何图形配置纹理映射方式
·加载纹理图像并对其进行配置
·在片元着色器中将相应的纹理像素抽取出来并赋给片元
2)纹理坐标:
使用纹理时,要指定纹理图像的哪部分覆盖到几何图形上,这需要使用纹理坐标。WebGL的纹理坐标是二维的,为了避免几种坐标的混淆,用s和t表示纹理坐标,也有的使用u和v来表示。
纹理图像的左下角为(0.0, 0.0),右下角为(1.0, 0.0),右上角为(1.0, 1.0),左上角为(0.0, 1.0)。纹理图像与自身尺寸无关,右上角纹理坐标都是(1.0, 1.0)。
在WebGL中使用纹理时,需要将纹理坐标与需要贴图的图形顶点坐标进行对应,对应关系要在代码中进行设置。常用的是将顶点坐标与纹理坐标写入同一缓冲区,如:
var verticesTexCoords = new Float32Array([-0.5, 0.5, 0.0, 1.0, -0.5, -0.5, 0.0, 0.0, 0.5, 0.5, 1.0, 1.0, 0.5, -0.5, 1.0, 0.0]);
数组中包括四个组,每个组包括4个数据,其中前两个是顶点坐标,后面两个点是纹理坐标。从数据中看出顶点(-0.5, 0.5)对应纹理坐标的(0.0, 1.0),顶点(-0.5, -0.5)对应纹理坐标的(0.0, 0.0),顶点(0.5, 0.5)对应纹理坐标的(1.0, 1.0),顶点(0.5, -0.5)对应纹理坐标的(1.0, 0.0)。
3)着色器中使用纹理:
为了在WebGL中使用纹理,首先要在着色器中进行相关设置。示例:
var VSHADER_SOURCE =
 'attribute vec4 a_Position;\n' +
 'attribute vec2 a_TexCoord;\n' +
 'varying vec2 v_TexCoord;\n' +
 'void main() {\n' +
 '  gl_Position = a_Position;\n' +
 '  v_TexCoord = a_TexCoord;\n' +
 '}\n';
var FSHADER_SOURCE =
 'precision mediump float;\n' +
 'uniform sampler2D u_Sampler;\n' +
 'varying vec2 v_TexCoord;\n' +
 'void main() {\n' +
 '  gl_FragColor = texture2D(u_Sampler, v_TexCoord);\n' +
 '}\n';
其中,顶点着色器中不仅使用了attribute变量,也使用了varying变量,两种变量都指定了纹理坐标,并指示varying变量引用attribute变量的值,这个值会传送到片元着色器中。片元着色器中同时使用了uniform变量和varying变量,varying变量与顶点着色器中的对应,指定纹理坐标,而uniform变量则是表示纹理,这个表示纹理的uniform变量声明为专用于纹理对象的数据类型sampler2D,对应gl.TEXTURE_2D数据类型。片元着色器中使用了内置函数texture2D(),此函数用来抽取纹理像素颜色。示例中将texture2D()函数的返回值赋值给片元着色器的内置变量gl_FragColor,片元着色器就将当前片元变为对应颜色,纹理图像就被映射到了图形。
4)JavaScript脚本中使用纹理:
使用上述着色器代码的JavaScript脚本为:
var canvas=document.getElementById('webgl');
var gl=getWebGLContext(canvas);
initVarShaders(gl,VSHADER_SOURCE,FSHADER_SOURCE)
var verticesTexCoords = new Float32Array([-0.5, 0.5, 0.0, 1.0, -0.5, -0.5, 0.0, 0.0, 0.5, 0.5, 1.0, 1.0, 0.5, -0.5, 1.0, 0.0]);
var n = 4;
var vertexTexCoordBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexTexCoordBuffer);
gl.bufferData(gl.ARRAY_BUFFER, verticesTexCoords, gl.STATIC_DRAW);
var FSIZE = verticesTexCoords.BYTES_PER_ELEMENT;
var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 4, 0);
gl.enableVertexAttribArray(a_Position);  
var a_TexCoord = gl.getAttribLocation(gl.program, 'a_TexCoord');
gl.vertexAttribPointer(a_TexCoord, 2, gl.FLOAT, false, FSIZE * 4, FSIZE * 2);
gl.enableVertexAttribArray(a_TexCoord);
gl.clearColor(0.0,0.0,0.0,1.0);

var texture = gl.createTexture();
var u_Sampler = gl.getUniformLocation(gl.program, 'u_Sampler');
var image = new Image();
image.onload = function(){ loadTexture(gl, n, texture, u_Sampler, image); };
image.src = 'sky.jpg';
上述代码的前面一部分与不使用纹理的代码基本一致,只是因为顶点着色器代码中使用了attribute变量顶点坐标a_Position和纹理坐标a_TexCoord,所以需要通过gl.getAttribLocation()方法获得相应坐标变量在缓冲区中的位置,然后使用gl.vertexAttribPointer()方法把坐标变量与缓冲区中的位置绑定,最后使用gl.enableVertexAttribArray()方法开启坐标变量。
后面一部分是使用纹理需要的处理流程。首先使用gl.createTexture()创建纹理对象,因为纹理在片元着色器中使用了uniform变量u_Sampler,所以需要使用gl.getUniformLocation()方法获取uniform变量在缓冲区的位置,然后创建一个image对象,指定加载图像的路径,声明图像加载onload完成后执行的函数。
纹理处理的主要部分都在纹理图像的加载函数 loadTexture()中,这是一个自定义的函数:
function loadTexture(gl, n, texture, u_Sampler, image) {
 gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1); // Flip the image's y axis
 gl.activeTexture(gl.TEXTURE0);
 gl.bindTexture(gl.TEXTURE_2D, texture);
 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
 gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image);
 gl.uniform1i(u_Sampler, 0);
 gl.clear(gl.COLOR_BUFFER_BIT);   // Clear <canvas>
 gl.drawArrays(gl.TRIANGLE_STRIP, 0, n); // Draw the rectangle
}
代码中首先对纹理图像进行y轴反转,接着开启0号纹理单元,再绑定纹理对象,然后配置纹理参数、配置纹理图像,然后就可以将0号纹理传递给着色器,最后使用gl.drawArrays()方法绘制出带纹理的图形。

3. 纹理处理中使用的函数:

上述代码中使用了几个专用于纹理的函数。
1)gl.createTexture():创建纹理对象以存储纹理图像
函数执行成功返回创建的纹理对象,否则返回null。
2)gl.deleteTexture(texture):删除指定的纹理对象
其中的参数texture为指定的纹理对象。
3)image.onload = function(){}:定义图像异步加载的函数
要加载作为纹理的图像需要先使用new Image()创建image对象,并使用image.src属性指定加载图像的路径。基于安全考虑,图像不能使用跨域纹理图像,只能从网页自身所在域的服务器上异步方式加载。图像加载完成后会执行定义的加载函数。
4)gl.pixelStorei(pname, param):使用pname和param指定的方式处理加载到的图像
其中,参数pname取值可以为gl.UNPACK_FLIP_Y_WEBGL或gl.UNPACK_PREMULTIPLY_ALPHA _WEBGL,分别表示对图像进行Y轴旋转和将图像RGB颜色值的每一个分量乘以A;参数param指定一个整数,0或非0,0表示false,非0表示true。
因为图片坐标系统的Y轴方向是向下的,与纹理坐标系统的Y轴方向不同,因此一般需要先将图像进行Y轴反转再贴纹理。
5)gl.activeTexture(texUnit):激活texUnit指定的纹理单元
其中的参数TeXUnit指定准备激活的纹理单元,gl.TEXTURE0、gl.TEXTURE1、......gl.TEXTURE7。WebGL的纹理对象至少支持管理8个纹理单元texture unit,每个纹理单元有一个编号,用来管理一张纹理图像。可以使用gl.TEXTURE_IMAGE_UNITS参数获取系统支持的纹理单元数。如果不调用gl.activeTexture()方法设置活动纹理图像单元,默认时使用gl.TEXTURE0纹理图像单元。
可以使用以下方法来获取活动纹理图像单元:
var activeTextureUnit=gl.getParameter(gl.ACTIVE_TEXTURE);
这个函数的返回值并不是0~7这类数值,而是十六进制的一系列值:
const GLenum TEXTURE0 =0x84C0;
const GLenum TEXTURE1 =0x84C1;
const GLenum TEXTURE2 =0x84C2;
const GLenum TEXTURE3 =0x84C3;
const GLenum TEXTURE4 =0x84C4;
const GLenum TEXTURE5 =0x84C5;
const GLenum TEXTURE6 =0x84C6;
const GLenum TEXTURE7 =0x84C7;
......
const GLenum TEXTURE31 =0x84DF;
6)gl.bindTexture(target, texture):开启texture指定的纹理对象并绑定到target上
如果已经通过gl.activeTexture()激活了某个纹理单元,则纹理对象也会绑定到这个纹理单位上。其中,参数target为gl.TEXTURE_2D或gl.TEXTURE_BUVE_MAP两种值,分别表示二维纹理和立方体纹理;参数texture表示绑定的纹理单元。
在WebGL中,不能直接操作纹理对象,必须通过将纹理对象绑定到纹理单元上,然后通过操作纹理单元来操作纹理对象。
7)gl.texParameteri(target, pname, param):配置纹理对象参数
此函数将param的值赋给绑定的目标纹理对象的pname属性上。其中,参数target为gl.TEXTURE_2D或gl.TEXTURE_BUVE_MAP两种值,pname为目标纹理对象的一个参数,param为纹理参数的值。
通过pname可以指定4个纹理参数:
·gl.TEXTURE_MAG_FILTER:放大方法,指定当纹理的绘制范围比纹理本身更大时如何获取纹理像素颜色。默认值为gl.LINEAR。
·gl.TEXTURE_MIN_FILTER:缩小方法,指定当纹理的绘制范围比纹理本身小时如何获取纹理像素颜色。默认值为gl.NEAREST_MIPMAP_LINEAR。
·gl.TEXTURE_WRAP_S:水平填充方法,指定如何对纹理图像左侧或右侧的区域进行填充。默认值gl.REPEAT。
·gl.TEXTURE_WRAP_T:垂直填充方法,指定如何对纹理图像上方和下方的区域进行填充。默认值gl.REPEAT。
可以赋给gl.TEXTURE_MAG_FILTER和gl.TEXTURE_MIN_FILTER的非金字塔纹理常量有:

gl.LINEAR

线性过滤模式,使用距离新像素中心最近的四个像素值的加权平均,作为新像素的值

gl.NEAREST

最近相邻过滤模式,使用原纹理上距离映射后像素中心最近的那个像素的颜色值作为新像素的值

gl.TEXTURE_MIN_FILTER还可以使用金字塔MIPMAP纹理,即Mip纹理链。
可以赋给gl.TEXTURE_WRAP_S和gl.TEXTURE_WRAP_T的常量有:

gl.REPEAT

平铺式的重复纹理

gl.MIRRORED_REPEAT

镜像对称式的重复纹理

gl.CLAMP_TO_EDGE

使用纹理图像边缘值

8)gl.texImage2D(target, level, internalformat, format, type, image):将纹理对象分配给纹理
将image指定的图像分配给绑定到目标上的纹理对象。其中,参数target为gl.TEXTURE_2D或gl.TEXTURE_BUVE_MAP两种值;level是为金字塔纹理准备的,一般传入0; internalformat为图像的内部格式;format为纹理数据的格式;type为纹理数据的类型;image为包含纹理的Image对象,其实也可以使用canvas对象和视频对象。WebGL中,internalformat和format两种格式必须一致。
图像的内部格式internalformat和纹理数据的格式format可以使用的格式有:

gl.RGB

红、绿、蓝

gl.RGBA

红、绿、蓝、透明度

gl.ALPHA

(0.0, 0.0, 0.0, 透明度)

gl.LUMINANCE

L、L、L、1L:流明

gl.LUMINANCE_ALPHA

L、L、L:透明度

流明表示感知到的物体表面亮度,通常使用物体表面红、绿、蓝分量值的加权平均。
type参数指定的数据类型有:

gl.UNSIGNED_BYTE

无符号整数,每个颜色分量占居1字节

gl.UNSIGNED_SHORT_5_6_5

RGB:每个分量分别占居5、6、5比特

gl.UNSIGNED_SHORT_4_4_4_4

RGBA:每个分量分别占居4、4、4、4比特

gl.UNSIGNED_SHORT_5_5_5_1

RGBA:RGB每个分量各占居5比特,A分量占居1字节

图像格式参数和数据类型参数的各组组合为:

图像格式

类型

每个纹理像素的字节数

gl.RGB

gl.UNSIGNED_BYTE

3

gl.RGBA

gl.UNSIGNED_BYTE

4

gl.ALPHA

gl.UNSIGNED_BYTE

1

gl.LUMINANCE

gl.UNSIGNED_BYTE

1

gl.LUMINANCE_ALPHA

gl.UNSIGNED_BYTE

2

gl.RGB

gl.UNSIGNED_SHORT_5_6_5

2

gl.RGBA

gl.UNSIGNED_SHORT_4_4_4_4

2

gl.RGBA

gl.UNSIGNED_SHORT_5_5_5_1

2

9)gl.uniform1i(uniformVar, texUnit):指定纹理单元编号并传给对应的纹理uniform变量
其中,参数uniformVar为片元着色器中定义的纹理uniform变量,texUnit为激活的一个纹理单元编号。要通过此函数指定纹理单元编号与纹理对象绑定,然后片元着色器就可以访问纹理图像了。
使用纹理时,片元着色器中要使用专用的一种数据类型sample2D,而且要使用uniform变量并设为这种数据类型。JavaScript脚本中先使用 gl.getUniformLocation()获取这个uniform变量在缓冲区中的位置,开启纹理单元并绑定纹理对象后,就可以使用gl.uniform1i()方法将纹理传给uniform变量,着色器就能使用纹理图像了。
10)texture2D(sampler2D sampler, vec2 coord):从指定纹理上获取指定纹理坐标处的颜色
这个函数是在片元着色器中使用的,是GLSL ES的内置函数,这个函数用来从sampler指定的纹理上获取coord指定的纹理坐标处的像素颜色。其中,参数sampler指定纹理单元编号,coord指定纹理坐标。返回值是vec4类型的数据,为纹理坐标处像素的颜色值,颜色值有多种格式,由gl.textImage2D()方法中的internalformat决定,如果纹理图像不可用则返回(0.0, 0.0, 0.0, 1.0)。
texture2D()函数的返回值格式:

internalformat

返回值

gl.RGB

(R, G, B, 1.0)

gl.RGBA

(R, G, B, A)

gl.ALPHA

(0.0, 0.0, 0.0, 1.0)

gl.LUMINANCE

(L, L, L, 1.0)

gl.LUMINANCE_ALPHA

(L, L, L, A)

片元着色器代码中,texture2D()函数的参数sampler来自uniform变量,而坐标参数coord则来自varying变量,也就是从顶点着色器经内插传来的值,顶点着色器中的原值是纹理坐标。代码中把texture2D()函数的返回值赋给片元着色器的内置变量gl_FragColor,片元着色器就根据获取的纹理坐标及颜色把当前片元渲染成得到的颜色。

4. Mip映射纹理:

当一个像素对应很多纹理像素时,不管是最近相邻过滤还是线性过滤,都会产生锯齿问题,解决方法是使用小尺寸的纹理,这样每个像素不会对应很多像素。但对靠近观察者的物体,小尺寸纹理会出现纹理拉伸现象,看起来有块状效果或模糊效果。
Mip纹理使用一系列纹理,从基础的0级纹理开始,其他纹理依次是前一个纹理的一半大小,这一系列纹理形成纹理链,总体要多占用1/3的内存空间。Mip纹理不必是正方形,但纹理链上的纹理必须一直继续到最后一个纹理的大小为1x1为止。
在WebGL中应用Mip纹理只需要载入基础纹理作为0级纹理,然后调用gl.generateMipmap()方法会自动生成整个Mip映射纹理链。也可以用一个图像编辑工具离线生成Mip纹理链上的全部图像,然后用gl.texImage2D()方法载入每幅图像。为了使用Mip映射纹理,基础纹理的长和宽都必须是2的次方。
使用Mip纹理链后,gl.LINEAR和gl.NEAREST模式都使用基础纹理,其中:
·gl.LINEAR:将线性过滤应用与基础纹理,这是指纹理坐标的颜色是由相邻4个纹理像素的颜色的加权平均值得到的。
·gl.NEAREST:将最近相邻过滤模式应用于基础纹理,这是指使用最接近的纹理像素的颜色作为纹理坐标的颜色。
纹理收缩gl.TEXTURE_MIN_FILTER还可以选择4种新模式:
·gl.NEAREST_MIPMAP _NEAREST:选择最近的Mip映射级,并且在这个映射级中使用最近相邻过滤。
·gl.NEAREST_MIPMAP_LINEAR:选择两个最近的Mip映射级,然后在每个Mip映射级中使用最近相邻过滤得到两个中间结果,对这两个中间结果进行线性插值得到纹理坐标的最终颜色。
·gl.LINEAR_MIPMAP_NEAREST:选择最近的Mip映射级,然后在每个Mip映射级中选择线性过滤。
·gl.LINEAR_MIPMAP_ LINEAR:选择两个最近的Mip映射级,在这两个Mip映射级中用线性过滤得到两个中间结果,对这两个结果进行线性插值,得到最终值。此方法生成最好的效果,但需要执行更多的运算,也称为三线性。

5. 多幅纹理的使用:

WebGL可以同时处理多幅纹理,通过其中的纹理单元分别存储。片元着色器示例代码:
var FSHADER_SOURCE =
 'precision mediump float;\n' +
 'uniform sampler2D u_Sampler0;\n' +
 'uniform sampler2D u_Sampler1;\n' +
 'varying vec2 v_TexCoord;\n' +
 'void main() {\n' +
 '  vec4 color0 = texture2D(u_Sampler0, v_TexCoord);\n' +
 '  vec4 color1 = texture2D(u_Sampler1, v_TexCoord);\n' +
 '  gl_FragColor = color0*color1;\n' +
 '}\n';
代码中又加入一个uniform变量,以便使用另一个纹理。两个纹理的经texture2D()函数处理后返回值分别赋值给vec4类型的两个变量,两个值相乘后赋值给内置变量gl_FragColor。
JavaScript脚本代码为:
var g_texUnit0=false, g_texUnit1=false;
var texture0 = gl.createTexture();
var texture1 = gl.createTexture();
var u_Sampler0 = gl.getUniformLocation(gl.program, 'u_Sampler0');
var u_Sampler1 = gl.getUniformLocation(gl.program, 'u_Sampler1');
var image0 = new Image();
var image1 = new Image();
image0.onload = function(){ loadTexture(gl, n, texture0, u_Sampler0, image0, 0); };
image1.onload = function(){ loadTexture(gl, n, texture1, u_Sampler1, image1, 1); };
image0.src = 'pic1jpg';
image1.src = 'pic2.jpg';
代码中创建了两个纹理对象,又分别获取片元着色器中两个uniform变量在缓冲区中的位置,然后创建两个图像对象,并把两个纹理图像的路径赋给分别赋给两个图像变量的src属性,还为两个图像对象定义了加载函数。而对两个图像纹理的操作在加载函数中:
function loadTexture(gl, n, texture, u_Sampler, image, texUnit) {
 gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1); // Flip the image's y axis
 if(texUnit==0){
  gl.activeTexture(gl.TEXTURE0);
  g_texUnit0=true;
 }else{
  gl.activeTexture(gl.TEXTURE1);
  g_texUnit1=true;
 }
 gl.bindTexture(gl.TEXTURE_2D, texture);
 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
 gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
 gl.uniform1i(u_Sampler, texUnit);
 gl.clear(gl.COLOR_BUFFER_BIT);   // Clear <canvas>
 if(g_texUnit0 && g_texUnit1)
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, n); // Draw the rectangle
}
纹理图像加载函数中,也是首先对纹理图像进行y轴反转,接着开启纹理单元,再绑定纹理对象,然后配置纹理参数、配置纹理图像,然后就可以将纹理单元传递给着色器,最后使用gl.drawArrays()方法绘制出带纹理的图形。区别只是对不同的纹理图像使用了分别开启了两个纹理单元0和1,当两个纹理单元都开启后,才最终绘制图像。

6. 跨域资源共享获取纹理:

所谓同域策略same-origin policy,是在浏览器中运行的一个脚本不允许从另一个域获取数据。对于脚本、文档、图像等,如果包含这两个资源都源自同一个域名、都使用一种模式访问、且通过同一个端口号访问,3个条件都满足,就被认为是同域的。
一个HTTP页面可以显示来自另一个域的图像。可以使用JavaScript程序创建一个Image对象,从另一个域读取图像数据并放入这个Image对象,最后把这个Image对象绘制在HTML画布canvas上。但是这幅图像在用context2D.drawImage()方法绘制到画布上后,会作一个标志,从而不允许脚本用canvas.toDataURL()方法对画布中的数据做序列化处理。
一般情况下,canvas.toDataURL()方法以data:UTL返回一个地址,PNG图像以base64的编码字符串内联编码到这个地址中,这个字符串格式为:data:image/png;base64,iVBORw0KGgoAAA...ErkJggg=="。base64编码的数据是以iVBOR开始的,这个字符串可以赋值给<img>标记的src属性,通过它显示在浏览器中,这时JavaScript代码也可以访问这幅图像的像素数据。
一般情况下,WebGL不允许用gl.texImage2D()或gl.texSubImage2D()方法把跨域图像或视频上传为纹理。如果需要使用另一个域上的图像或视频,可以使用跨域资源共享CORS(Cross-Origin Resource Sharing)与保存有图像和视频的服务器进行协作,引用这些图像和视频作为纹理。
跨域资源共享CORS是不同域之间实现可控通信的一个通用方法,当浏览器想要通过脚本使用来自另一个域上的服务器中的资源时,就在给服务器的请求中发送一个特殊的HTTP报头origin;如果服务器允许就在响应中插入另一个HTTP报头Access-Control-Allow-Origin并表示允许访问这个资源。WebGL可以使用CORS来实现跨域纹理。
使用跨域资源共享CORS获取图像,在给Image对象的src属性设置图像的URL之前把Image对象的crossOrigin属性设为anonymous:
image.crossOrigin="anonymous";
image.src=url;
而使用CORS请求数据时,往往还需要用canvas.toDataURL()方法读取画布中的像素:
var canvas=document.getElementById("canvas");
if (canvas.getContext) {
 var context2D=canvas.getContext("2d");
 var img=new Image();
 img.onload=function(){
  context2D.drawImage(img, 0, 0);
  var pixelData=canvas.toDataURL();
 }
}
image.crossOrigin="anonymous";
image.src="http://other-domain.com/image.jpg";

六、三维视图:

前面的示例都是简化为绘制二维图形,对三维图形绘制又会有一些特别的方面。三维物体有深度,也就是Z轴,但最后还是要把三维场景绘制到二维的屏幕上,即绘制观察者看到的世界。

1. 视线和视点:

观察者可以处在任意位置观察,将观察者所处的位置称为视点eye point,从视点出发沿着观察方向的射线称作视线viewing direction。要把观察到的景象绘制到屏幕上,还需要知道上方向up direction。有了上面三项信息,就可以确定观察者的状态了。
在WebGL系统中,默认情况下的视点处于原点(0.0, 0.0, 0.0),视线为Z轴负半轴,即指向屏幕内部。
视点坐标常用(eyeX, eyeY, eyeZ)表示,观察点坐标可以用(atX, atY, atZ)来表示,而上方向使用(upX, upY, upZ)来表示。在WebGL中,可以使用上述三个矢量创建一个视图矩阵view matrix,然后将该矩阵传给顶点着色器,这个矩阵称为视图矩阵。可以将这种常用的视图矩阵形成方法做成一个函数以方便使用,如setLookAt ()。示例代码:
Matrix4.prototype.setLookAt = function(eyeX, eyeY, eyeZ, centerX, centerY, centerZ, upX, upY, upZ) {  
  var e, fx, fy, fz, rlf, sx, sy, sz, rls, ux, uy, uz;
  fx = centerX - eyeX;  fy = centerY - eyeY;  fz = centerZ - eyeZ;
  rlf = 1 / Math.sqrt(fx*fx + fy*fy + fz*fz);
  fx *= rlf;  fy *= rlf;  fz *= rlf;
  sx = fy * upZ - fz * upY;  sy = fz * upX - fx * upZ;  sz = fx * upY - fy * upX;
  rls = 1 / Math.sqrt(sx*sx + sy*sy + sz*sz);
  sx *= rls;  sy *= rls;  sz *= rls;
  ux = sy * fz - sz * fy;  uy = sz * fx - sx * fz;  uz = sx * fy - sy * fx;
  e = this.elements;
  e[0] = sx;  e[1] = ux;  e[2] = -fx;  e[3] = 0;
  e[4] = sy;  e[5] = uy;  e[6] = -fy;  e[7] = 0;
  e[8] = sz;  e[9] = uz;  e[10] = -fz;  e[11] = 0;
  e[12] = 0;  e[13] = 0;  e[14] = 0;  e[15] = 1;
  return  translate(-eyeX, -eyeY, -eyeZ);
};
用视图矩阵乘以顶点坐标会把顶点变换到合适位置,使得观察者观察新位置的顶点,就好像在观察者处在视图矩阵描述的视点上观察原始顶点一样。从视点上看的新顶点坐标可以用“视图矩阵x模型矩阵x原始顶点坐标”的算式得到。

2. 可视范围:

虽然可以将三维物体放在三维空间的任何位置,但只有当它在可视范围内时,WebGL才会绘制它。人眼的水平视角大约为200度左右,WebGL也是以类似的方式处理。除了水平和垂直范围的限制,WebGL还限制观察者的可视深度,即能够看多远。使用水平视角、垂直视角和可视深度,定义了可视空间view volume。
常用的可视空间有两类:
·正射投影orthographic projection:这是长方体可视空间,也称盒状空间
·透视投影perspective projection:四棱锥可视空间
透视投影下,产生的三维场景看上去有深度感,是人眼平时观察世界的方式。正射投影中物体看上去的大小与其所在的位置没有关系,方便比较场景中物体的大小,是平面制图常用的方式。
可视空间由前后两个矩形平面确定,分别称近裁剪面near clipping plane和远裁剪面far clipping plane,前者的四个顶点为(right, top, -near), (-left, top, -near), (-left, -bottom, -near), (right, -bottom, -near);而后者的四个顶点为(right, top, far), (-left, top, far), (-left, -bottom, far), (right, -bottom, far)。近裁剪面与远裁剪面之间的的空间就是可视空间,只有在此空间内的物体会被显示出来。
1)盒状可视空间:
定义盒状可视空间,可以使用glMatrix函数库的函数:
mat4.ortho(left, right, bottom, top, near, far, projecttionMatrix);
也可以使用其他函数库的函数或自定义的函数,如setOrtho()函数,代码为:
Matrix4.prototype.setOrtho = function(left, right, bottom, top, near, far) {
  var e, rw, rh, rd;
  if (left === right || bottom === top || near === far) { throw 'null frustum';  }
  rw = 1 / (right - left);  rh = 1 / (top - bottom);  rd = 1 / (far - near);  e = this.elements;
  e[0]  = 2 * rw;  e[1]  = 0;  e[2]  = 0;  e[3]  = 0;
  e[4]  = 0;  e[5]  = 2 * rh;  e[6]  = 0;  e[7]  = 0;
  e[8]  = 0;  e[9]  = 0;  e[10] = -2 * rd;  e[11] = 0;
  e[12] = -(right + left) * rw;  e[13] = -(top + bottom) * rh;
  e[14] = -(far + near) * rd;  e[15] = 1;
  return this;
};
上面定义的方法返回一个矩阵,称为正射投影矩阵。矩阵为:
正交投影矩阵
使用盒状可视空间,canvas上显示的是可视空间中的物体在近裁剪面上的投影。如果裁剪面的宽高比和canvas的不一样,画面就会被按照canvas的宽高比进行压缩,物体会被扭曲。
2)透视投影空间:
透视投影可视空间是一种锥台形状,好像去掉顶部的金字塔,有两种计算投影矩阵的方法:
mat4.frustum(left, right, bottom, top, near, far, projecttionMatrix);
mat4.perspective(fvoy, aspect, near, far, projecttionMatrix);
上面是glMatrix函数库中的方法,也可以使用其他类似函数库或自定义投影矩阵计算函数:
Matrix4.prototype.setFrustum = function(left, right, bottom, top, near, far) {
  var e, rw, rh, rd;
  if (left === right || top === bottom || near === far) { throw 'null frustum'; }
  if (near <= 0) { throw 'near <= 0'; }
  if (far <= 0) { throw 'far <= 0'; }
  rw = 1 / (right - left);  rh = 1 / (top - bottom);  rd = 1 / (far - near);  e = this.elements;
  e[ 0] = 2 * near * rw;  e[ 1] = 0;  e[ 2] = 0;  e[ 3] = 0;
  e[ 4] = 0;  e[ 5] = 2 * near * rh;  e[ 6] = 0;  e[ 7] = 0;
  e[ 8] = (right + left) * rw;  e[ 9] = (top + bottom) * rh;
  e[10] = -(far + near) * rd;  e[11] = -1;
  e[12] = 0;  e[13] = 0;  e[14] = -2 * near * far * rd;  e[15] = 0;
  return this;
};
Matrix4.prototype.setPerspective = function(fovy, aspect, near, far) {
  var e, rd, s, ct;
  if (near === far || aspect === 0) { throw 'null frustum'; }
  if (near <= 0) { throw 'near <= 0'; }
  if (far <= 0) { throw 'far <= 0'; }
  fovy = Math.PI * fovy / 180 / 2;
  s = Math.sin(fovy);
  if (s === 0) { throw 'null frustum'; }
  rd = 1 / (far - near);  ct = Math.cos(fovy) / s;  e = this.elements;
  e[0]  = ct / aspect;  e[1]  = 0;  e[2]  = 0;  e[3]  = 0;
  e[4]  = 0;  e[5]  = ct;  e[6]  = 0;  e[7]  = 0;
  e[8]  = 0;  e[9]  = 0;  e[10] = -(far + near) * rd;  e[11] = -1;
  e[12] = 0;  e[13] = 0;  e[14] = -2 * near * far * rd;  e[15] = 0;
  return this;
};
其中,frustum()和setFrustum()方法中的参数与盒式视图空间的方法中的参数含义基本一致,透视投影空间还有另外的方法perspective()和setPerspective (),其中aspect指定近裁剪面的宽高比,fovy指定垂直角,near,和far都必须大于0,用来指定近裁剪面和远裁剪面的位置。通过setPerspective方法获得的投影矩阵为:
透视投影矩阵
透视投影矩阵实际上将金字塔状的可视空间变换成了盒状的可视空间,经过投影矩阵变换的顶点坐标都必须在规范立方体中,这样才能在屏幕上显示出来。

3. 前后关系与隐藏面消除:

默认情况下,WebGL为了加速绘图操作,是按照顶点在缓冲区中的顺序来处理,如果距离近的顶点先绘制而距离远的顶点后绘制,后绘制的距离远的物体反而遮住近的物体。
为此,WebGL提供了隐藏面消除(hidden surface removal)功能。使用隐藏面消除的步骤:
·开启隐藏面消除功能:使用gl.enable(gl.DEPTH_TEST);
·绘制前清除深度缓冲区:使用gl.clear(gl.DEPTH_BUFFER_BIT);
如果要使用隐藏面消除,就必须知道每个几何图形的深度信息,而深度缓冲区就是用来存储深度信息的。由于深度方向通常是Z轴方向,有时候也称为Z缓冲区。在绘制任意一帧之前,都必须清除深度缓冲区,以消除绘制上一帧时在其中留下的痕迹,否则可能出现错误结果。
当然也需要清除颜色缓冲区,可以写在一起:
gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);

4. 多边形偏移:

使用隐藏面消除,当几何图形或物体的两个表面非常接近时会出现斑驳的表面,这称为深度冲突(Z fighting)。原因是,因为两个表面过于接近,深度缓冲区有限的精度无法区分哪个在前哪个在后。
WebGL提供了一种多边形偏移(polygon offset)机制,该机制自动在Z值加上一个偏移量,偏移量的值由物体表面相对于观察者视线的角度决定。启用该机制的步骤:
·启用多边形偏移:使用gl.enable(gl.POLYGON_OFFSET_FILL);
·在绘制之前指定计算偏移量的参数:使用gl.polygonOffset(1.0, 1.0);
gl.polygonOffset(factor, units)方法指定加到每个顶点绘制后Z值上的偏移量,偏移量按照公式m*factor+r*units计算,其中m表示顶点所在表面相对于观察者的视线的角度,而r表示硬件能够区分两个Z值之差的最小值。

5. 光照:

现实世界中,物体被光线照射会反射一部分光,只有当反射光线进入观察者的眼睛时才能够看到物体并辨认出其颜色。现实世界中,当光线照射到物体上时,发生了两个重要的现象:
·根据光线和光线方向,物体不同表面的明暗变得不一致
·根据光源和光线方向,物体向地面投下了影子
正是明暗的差别给了物体立体感。单一颜色的物体,向着光的表面看上去亮一些,而侧着光或背着光的表面看上去就暗一些。canvas上绘制的物体有了这些差异,看上去才像一个实际物体。
1)局部光照模型:
在3D图形中模拟灯光效果,可以使用全局光照效果或局部光照模型。全局光照模型考虑了来自其他物体反射的光,还有反射光又照射另一物体的光。全局光照模型可以产生极为真实的场景,但是需要大量的计算机资源,一般用于静止图像或制作电影中。
局部光照模型只考虑直接从已选定光源发射的光,模型中的物体不会被其他物体的反射光照射,也不会阻挡照射到它的光线,也就是不会自动创建阴影。
由于WebGL通常用于实时3D图形中,因此常使用的是局部光照模型。
2)Phong反射模型:
WebGL中常用的局部光照模型是Phong反射模型。Phong反射模型是Bui Tuong Phong于1975年开发并发表的模型。Phong反射模型中,一个点的最终颜色由环境光、漫射光、镜面光3个不同的反射分量组成,即一个片元的颜色可以描述为:
总反射=环境反射+漫反射+镜面反射
而人眼看到的物体的颜色就是由这个物体表面反射的光决定的,也就是决定于总反射。
场景中的物体的材质都相应有环境光反射率、漫反射率和镜面反射率3个属性,还有光泽度属性,用来计算镜面反射分量。计算公式中的环境反射、漫反射、镜面反射是光与相应分量的材质属性相互作用的结果。
3)环境反射ambient reflection:
环境光是在场景中反射多次以至于无法确定来自某个方向的光,结果是一个物体的各个侧面都会被环境光照射。如果环境光使用Ia表示,材质属性Ka,从该材质表面反射的环境光为:
I=Ka x Ia
其中乘法是基于各个分量的乘法,把材质属性的红色分量乘以光的红色分量,材质属性的绿色分量乘以光的绿色分量,材质属性的蓝色分量乘以光的蓝色分量。使用着语言表述为:
vac3 ambientReflection=uAmbientMaterial * uAmbientLight;
环境光并没有考虑光的位置和方向,也没有考虑观察方向。环境光照射物体的方式是各方向均匀的、强度相等的,也就是环境反射光是各向均匀的。Phong反射模型是局部光照模型,不直接考虑反射光照射其他物体,而是使用环境光来补偿这一点,环境光可以看作已被多次反射的光。
4)漫反射diffuse reflection:
现实中的大部分物体的材质的表面都是粗糙的,这种情况下反射光会以不固定的角度发射出去,反射光在各个方向上是均匀的,从任何角度看上去其强度都相等,据此建立理想反射模型。漫反射材质属性Kd,漫射光分量为Id,漫反射还要考虑入射光方向,用角度θ表示。可以用两种方法表示漫反射,第一种为:
I=Kd x Id x max(cosθ, 0)
其中,角度θ定义为表面法线n与入射光方向l的最小夹角。公式中的max()函数指定将cosθ的负值限定为0,即把负值设置为0。从漫反射的方程中可以看出,当光线与法线的夹角θ为0°时反射光最强;当θ为90°时表示光是从侧边发射过来,没有反射光。当θ大于90°时,cosθ为负值,这些光线不会照射物体表面,因此只考虑cosθ的非负情形,使用max()函数限定最小值为0。
当法线n和光线I方向都为归一化矢量时,这两个矢量的点积就为cosθ,所以有公式:
I=Kd x Id x max( n·I , 0)
这是漫反射的第二个公式。
着色语言中有内置函数dot()来计算点积,所以这个公式常用于WebGL应用程序中:
float diffuseLightWeightning=max(dot(normalEye, vectorToLightSource), 0.0);
vec3 diffuseReflectance=uDiffuseMaterial * uDiffuseLight * iffuseLightWeightning;
5)镜面反射:
如果有光滑的对象,光照射到这个对象上时会在上面产生明亮的斑点或强光,这是镜面反射。镜面光的反射类似于光线在平面镜上的反射,其中大部分光按特定的方向反射,因此视线方向非常重要。
来自某个方向的光线用矢量I表示,方向指向光源;表面法线用n表示,通常所有光线都沿着r方向反射,对于不太光滑的物体,反射光分散在矢量r周围。矢量v指向观察者,当v与r之间的夹角θ为0时,绝大部分光线向观察者方向发射;反射矢量r与观察者矢量v的夹角θ越大,反射到观察者的光就越少,通常这个量的下降快慢与cosθ的ɑ次方成正比,这里ɑ是材质的光泽度。计算镜面反射光I的公式为:
phong
其中,Ks为镜面材质属性,Is为传入光的镜面光分量,θ为观察者矢量v与反射矢量r之间的夹角,ɑ表示光泽度shininess。也可以使用点积来改写镜面反射公式:
phong
而计算反射矢量r的公式为:
r=2(l·n)n - l
并不需要手工计算反射矢量,着色语言内置了函数reflect(),可以根据矢量l和法线n计算反射矢量r,但其中假定矢量l指定光的方向是从光源到物体表面。着色语言代码为:
float rdotv=max(dot(reflectionVector, viewVectorEye), 0.0);
float specularLightWeight=pow(rdotv, shininess);
vec3 specularReflection=uSpecularMaterial * uSpecularLight * specularLightWeight;
6)光泽度shininess:
镜面反射中要使用光泽度ɑ,ɑ越大表明光泽度越高,意味着反射光的强度随矢量r与矢量v之间夹角θ增大而快速降低;ɑ值较小表明光泽度低,意味着反射光强度随矢量r与矢量v之间夹角θ的增大而缓慢降低。
光强因子随角度θ和光泽度ɑ的变化:

角度θ

ɑ =2

ɑ =8

ɑ =32

ɑ =64

0

1

1

1

1

10°

0.97

0.88

0.61

0.38

20°

0.88

0.61

0.14

0.02

30°

0.75

0.32

0.01

0.00

40°

0.59

0.12

0.00

0.00

50°

0.41

0.03

0.00

0.00

60°

0.25

0.00

0.00

0.00

70°

0.12

0.00

0.00

0.00

80°

0.03

0.00

0.00

0.00

90°

0.00

0.00

0.00

0.00

光泽度只影响光随r与v之间夹角增大而下降的速度,也就是只影响镜面反射的强光范围的大小,而不会影响其强度。
7)Phong反射模型的完整公式:
前面介绍了Phong反射模型的各个反射分量,组合在一起,就是Phong反射模型的完整公式:
phong
其中,计算反射矢量r的数学公式为:r=2(l·n)n - l。
公式中的各个参数为:
·Iphong----最终得到的光强
·Ka----环境材质属性,即反射系数,该值越大表明反射光越强
·Ia----环境光分量
·Kd----漫反射材质系数,该值越大表明反射光越强
·Id----漫反射分量
·n----表面法线的单位矢量
·l----光线的单位矢量,方向指向光源
·Ks----镜面反射材质属性系数,该值越大表明反射光越强
·Is----镜面光分量
·r----反射光单位矢量
·v----视线单位矢量,指向观察者
·ɑ---材质的光洁度
如果有多个光源,需要把它们各自的贡献添加到总光强中。
需要注意,表面法线n、光线方向矢量l、反射光矢量r、视线矢量v是用来使用点积来计算cosθ,都需要使用归一化后的单位矢量。着色语言有内置的归一化函数normalize()。如果单位矢量只是经过了旋转和平移矩阵进行变换,并没有改变矢量的长度,可以不再进行归一化,否则变换后必须经过归一化再进行相应操作。
8)Phong光照模型的着色器示例代码:
attribute  vec3  aVertexPosition;
attribute  vec3  aVertexNormal;
uniform  mat4  uMVMatrix;
uniform  mat4  uPMatrix;
uniform  mat3  uNMatrix;
uniform  vec3  uLightPosition;
uniform  vec3  uAmbientLightColor;
uniform  vec3  uDiffuseLightColor;
uniform  vec3  uSpecularLightColor;
varying  vec3  vLightWeighting;
const  float  shininess=32.0;
void  main() {
  vec4  vertexPositionEye4=uMVMatrix * vec4(aVertexPosition, 1.0);
  vec3  vertexPositionEye3=vertexPositionEye4.xyz/vertexPositionEye4.w;
  vec3  vectorToLightSource=normalize(uLightPosition-vertexPositionEye3);
  vec3  normalEye=normalize(uNMatrix * aVertexNormal);
  float  diffuseLightWeightning=max(dot(normalEye, vectorToLightSource), 0.0);
  vec3  reflectionVector=normalize(reflect(-vectorToLightSource, normalEye));
  vec3  viewVectorEye=-normalize(vertexPositionEye3);
  float  rdotv=max(dot(reflectionVector, viewVectorEye), 0.0);
  float  specularLightWeightning=pow(rdotv, shininess);
  vLightWeighting=uAmbientLightColor+uDiffuseLightColor * diffuseLightWeightning
  +uSpecularLightColor * specularLightWeightning;
  gl_Position=uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);
}
上面是顶点着色器的示例代码,没有使用纹理的片元着色器相对就比较简单:
precision  mediump  float;
varying  vec3  vLightWeighting;
void  main() {
gl_FragColor=vec4(vLightWeighting.rgb, 1.0);
}
9)光源类型:
真实世界中的光主要有三种类型,平行光directional light,类似于自然中的太阳光;点光源光point light,类似于人造灯泡的光,向各个方向发光;聚光源spot light,在某个方向按圆锥形状发射光。
⑴平行光:
平行光的光源距被照射表面无限远,照射到物体表面的光线彼此平行,光线的方向对表面上的顶点和片元都相同。平行光用一个方向和颜色来定义。计算平行光的漫反射可以直接计算法线矢量与指向光源矢量的点积:
float  diffuseLightWeighting=max(dot(vNormalEye, vectorToLightSource), 0.0);
⑵点光源光:
点光源位于某个特定位置且向各个方向发射光,需要指定光源位置和颜色。有时点光源会使用衰减模型。
光的强度通常随着光在介质中的传播距离衰减,光强衰减与距离的平方的倒数成正比。计算机图形学中,使用这种衰减模式会造成亮度的变化太大,一般使用的衰减函数为:
phong
其中,constantAtt为衰减常量,linearAtt为线性衰减,quadratticAtt为二次方衰减,r为距离光源的距离。当所有衰减常量为1时和二次方衰减常量为0.1时的衰减函数分别为:
phong          phong
着色器中使用衰减函数时,在顶点着色器中计算距离,并把该值发送到片元着色器,然后在片元着色器中计算衰减光强。着色器中有内置length()函数用于计算矢量的长度。更精细的使用衰减模型是在片元着色器中计算距离。
⑶聚光源:
聚光源使用spotDirection表示聚光源的方向,沿着光锥的轴方向;lightDirection表示聚光源到片元的方向,光照计算就是针对这个方向。聚光源还使用两个角度,θ表示spotDirection与lightDirection之间的夹角;ɑ表示聚光源的截面角度。当θ大于ɑ时,目标点位于光轴外,没有被聚光源照射到。
如果点位于光锥内,则spotDirection方向光强最大,然后随着θ值的增大逐渐衰减,常用的方法是用cosθ的spotExponent幂表示其衰减过程。光锥中光强衰减效果为spotEffect:
phong
如果在顶点着色器中实现聚光源并且应用Phong着色模式,与点光源没有区别,变化是在片元着色器中,但变化很小。
10)用于着色的不同插值方法:
着色是决定一个顶点、图元或物体的颜色的过程,需要用到光源的位置、物体距光源的距离、光的颜色、物体表面法线矢量、材质属性等参数。实际的WebGL应用中,通常会由许多三角形组成复杂的模型,着色过程通常包括光照模型和插值技术两部分,其中光照模型定义了如何利用光的颜色、材质和不同角度计算指定点的颜色,也就是光照模型确定了某个点的颜色的计算公式。
插值技术是根据顶点位置的颜色确定物体上其他位置的颜色的过程。已知顶点颜色后,片元的颜色要通过顶点颜色的线性插值确定,这个过程有几个方案,即平面着色、Gouraud着色、Phong着色。一般来说,平面着色最简单但结果最差;Gouraud着色需要很多计算,结果比较好;Phong着色最高级,结果最好。
①平面着色:
平面着色根据每个三角形的法线计算着色效果,根据法线着色只执行一次,整个三角形都采用计算结果的颜色。
使用平面着色的前提:
·光源无限远时,入射光线的角度在整个三角形上是一样的,也就是平行光下
·观察者无限远时,三角形上任何位置到观察者的角度都相同
·三角形代表一个平面时,如立方体的一个面,而不是曲面的近似表示时
上述情况下使用平面至少可以接受。通常情况下上述条件中的一个或多个不符合,这时使用平面着色就得不到满意效果,特别是使用三角形近似表示曲面一部分时,平面着色不是一个预期的光滑表面。
②Gouraud着色:
Gouraud着色以法国人Henri Gouraud命名,他于1971年公开发表了该方法。这个方法也称为逐顶点着色,因为着色是针对每个顶点计算的,而后对每个顶点的结果颜色进行线性插值得到片元颜色。
对于曲面,Gouraud着色产生的结果比平面着色好。通常,对于大多数粗糙的表面,Gouraud着色可以得到相当好的结果。然而对于光泽的表面,Gouraud着色会生成伪像,还因为着色公式只针对顶点计算,镜面高光会在顶点之间减弱,而忽略顶点间的镜面高光。
Phong着色:
Phong着色是以Bui Tuong Phong命名的,它对三角形的每个片元进行着色计算,这个方法也称为逐片元着色。
由于颜色是按片元着色的,因此得到的结果要好,特别是用于高光表面时。使用Phong着色,对于光亮物体,不会出现伪像,也不会忽略顶点内镜面光照射到的位置。
使用Phong着色,传送给顶点着色器的法线作为varying变量传送给片元着色器,所有varying变量都要经过线性插值计算,并利用线性插值得到的法线在片元着色器中计算每个片元的光照。对一个大平面,平面上各点距离光源距离不同,但顶点距离光源距离相同时使用Gouraud着色会使平面上各点光照一致,而使用Phong着色会得到更好的效果。
使用Phong着色时,光照计算转移到片元着色器中,顶点着色器需要执行的操作包括把顶点坐标和法线变换到全局坐标,并通过varying变量传给片元着色器。通常顶点着色器要把几何物体变换到投影坐标系,也要把纹理坐标传递给片元着色器。
11)光照映射light map:
光照映射常用于3D游戏中。如果场景中有静止的物体,可以创建包含预先计算好的光照信息的纹理,来模拟这些物体上的光照。这样,可以用全局光照模型(如辐射着色)计算得到高质量的光照效果,因为这时不需要进行实时计算。光照计算可以用Blender或Maya等3D建模软件事先离线完成,然后把结果保存在纹理中,并应用在WebGL应用程序中。
虽然可用同一个纹理保存基本纹理图像和光照信息,但更好的办法是使用另外一个名为光照映射的单独纹理,其中只包括光照信息的纹理。然后在片元着色器中把光照映射与基本纹理图像结合在一起。把光照纹理与基本纹理分开,光照效果可以重用于几个不同的基本纹理上,还可以根据需要修改基本纹理上的光照信息。此外,为了节省内存,砖墙等基本纹理经常使用拼贴tiled模式,按拼贴模式计算光照没有意义。
使用光照映射的着色器代码示例:
precision mediump float;
varying bec2 vTextureCoordinates;
uniform sampler2D uSamplerBase;
uniform sampler2D uSamplerLight;
void main() {
vec4 baseColor=texture2D(uSamplerBase, vTextureCoordinates);
vec4 lightValue=texture2D(uSamplerLight, vTextureCoordinates);
gl_FragColor=baseColor * lightValue;
}
因为着色器中增加了代码,所以JavaScript脚本中也需要添加相应代码,使用两个纹理,并传入相应的变量。
12)平行光源下的反射效果:
①平行光入射的漫反射:
平行光的方向是唯一的,对于同一个平面上的所有点,入射角是相同的。漫反射时其颜色与入射光在入射点的入射角θ有关,所以平行光入射的漫反射光颜色为:
漫反射光颜色=入射光颜色x表面基底色xcosθ
入射光的颜色可以是任何颜色,用RGB值来表示;物体表面的基底色是物体在标准白光(1.0, 1.0, 1.0)下的颜色。计算时对RGB值的三个分量逐个相乘。
②入射角:
为了获得算式中的入射角,就需要确定每个表面的朝向,再根据入射光的方向,然后计算入射角。物体表面的方向用其法向矢量的方向来表示,可以通过计算两个矢量的点积计算两个矢量的夹角余弦值,即:
cosθ=光线方向·法线方向
最后得到的算式为:
漫反射光颜色=入射光颜色x表面基底色x ( 光线方向·法线方向 )
使用时需要注意,光线方向矢量和表面法向矢量的长度要归一化,即使其长度为1。GLSL ES语言提供了内置的归一化函数和点积运算函数,可以直接使用。其中使用的光线方向实际上是入射方向的反方向,即从入射点指向光源方向,这时该方向与法线方向的夹角才是入射角。
③表面的朝向:
物体表面的朝向垂直于表面的方向,称为法向或法向矢量。法向矢量有三个矢量(nx, ny, nz),表示原点(0.0, 0.0, 0.0)指向点(nx, ny, nz)的方向。
每个表面实际有正面和背面两个面,两个面各自具有一个法向矢量。三维图形学中,表面的正面和背面取决于绘制表面时的顶点顺序,顺时针或逆时针。
计算好每个平面的法向矢量,然后将数据传给着色器程序。法向矢量数据也可以存储在缓冲区。
④着色器代码示例:
var VSHADER_SOURCE =
 'attribute vec4 a_Position;\n' +
 'attribute vec4 a_Color;\n' +
 'attribute vec4 a_Normal;\n' +        // Normal
 'uniform mat4 u_MvpMatrix;\n' +
 'uniform vec3 u_DiffuseLight;\n' +    // Diffuse light color
 'uniform vec3 u_LightDirection;\n' +  // Diffuse light direction
 'uniform vec3 u_AmbientLight;\n' +    // Color of an ambient light
 'varying vec4 v_Color;\n' +
 'void main() {\n' +
 '  gl_Position = u_MvpMatrix * a_Position;\n' +
 '  vec3 normal = normalize(a_Normal.xyz);\n' +
 '  float nDotL = max(dot(u_LightDirection, normal), 0.0);\n' +
 '  vec3 diffuse = u_DiffuseLight * a_Color.rgb * nDotL;\n' +
 '  vec3 ambient = u_AmbientLight * a_Color.rgb;\n' +
 '  v_Color = vec4(diffuse + ambient, a_Color.a);\n' +
 '}\n';
var FSHADER_SOURCE =
 '#ifdef GL_ES\n' +
 'precision mediump float;\n' +
 '#endif\n' +
 'varying vec4 v_Color;\n' +
 'void main() {\n' +
 '  gl_FragColor = v_Color;\n' +
 '}\n';
代码中,光照效果主要是在顶点着色器中处理的,首先定义了3个attribute变量,分别是顶点坐标、顶点基底颜色、表面法向归一化矢量;然后定义了4个uniform变量,分别为变换矩阵、入射光颜色、入射光方向、环境光颜色;还定义了1个varying变量。main()函数中,变换矩阵与顶点坐标的乘积赋值给内部变量gl_Position;接着归一化法向矢量,然后计算光线方向和法向矢量的点积,再计算漫反射的颜色,继而计算环境光颜色,最后将获得的漫反射颜色值与环境光颜色相加赋值给varying变量的前三个分量,其中的透明度分量取自顶点颜色的透明度。对法向矢量归一化时,因为原矢量是vec4类型,要取出前3个分量归一化。计算光线方向与法线方向的点积时,使用了内置点积函数dot()和max(),max()函数获取点积结果与0值中的较大值,点积小于0说明角度θ大于90°,也就是光线照射在表面的背面上。
⑤JavaScript代码:
var canvas = document.getElementById('webgl');
var gl = getWebGLContext(canvas);
!initVarShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE);
var n = initVertexBuffers(gl);
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.enable(gl.DEPTH_TEST);
var u_MvpMatrix = gl.getUniformLocation(gl.program, 'u_MvpMatrix');
var u_DiffuseLight = gl.getUniformLocation(gl.program, 'u_DiffuseLight');
var u_LightDirection = gl.getUniformLocation(gl.program, 'u_LightDirection');
var u_AmbientLight = gl.getUniformLocation(gl.program, 'u_AmbientLight');
gl.uniform3f(u_DiffuseLight, 1.0, 1.0, 1.0);
var lightDirection = new Vector3([0.5, 3.0, 4.0]);
lightDirection.normalize();      // Normalize
gl.uniform3fv(u_LightDirection, lightDirection.elements);
gl.uniform3f(u_AmbientLight, 0.2, 0.2, 0.2);
var mvpMatrix = new Matrix4();   // Model view projection matrix
mvpMatrix.setPerspective(30, canvas.width/canvas.height, 1, 100);
mvpMatrix.lookAt(3, 3, 7, 0, 0, 0, 0, 1, 0);
gl.uniformMatrix4fv(u_MvpMatrix, false, mvpMatrix.elements);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.drawElements(gl.TRIANGLES, n, gl.UNSIGNED_BYTE, 0);
这部分代码中,前面获取canvas及context,接着载入着色器代码shader,然后初始化顶点缓冲区、设置背景色、开启隐藏面消除功能,其中使用了自定义函数 initVertexBuffers(gl)。
后面一部分主要是处理光照效果的部分,先获取着色器中定义的uniform变量变换矩阵、入射光颜色、入射光方向、环境光颜色在缓冲区中的位置;然后将入射光颜色变量传入(1.0, 1.0, 1.0),即标准白光;下面是定义光线方向矢量,并对光线方向归一化,然后传入着色器;还需要将环境光颜色传入缓冲区;后面是转换矩阵处理,先设置投影空间,再根据视角进行保变换,然后传入着色器。
代码中使用了自定义函数 initVertexBuffers(gl),其中主要是定义了顶点坐标数组、顶点颜色数组、法向矢量数组和顶点索引数组,然后初始化并绑定缓冲区,继而获取attribute变量在缓冲区中的位置。这部分代码是与所绘制图形密切相关。
对运动物体的光照效果;
物体运动时,也就是经过平移、旋转及缩放变换,表面法向矢量会随之变换。要得到运动后的光照效果,首先需要计算法向矢量。变换后的法向矢量是变换前的法向矢量乘以模型矩阵的逆转置矩阵inverse transpose matrix。
求逆转置矩阵,要先求出原矩阵的逆矩阵,然后再进行转置。
求逆矩阵的函数可以使用glMatrix中的函数,或使用自定义函数;矩阵的转置可以使用glMatrix函数库的函数或自定义。为此,需要在着色器代码中增加对法向矢量的变换矩阵
var VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' +
  'attribute vec4 a_Color;\n' +
  'attribute vec4 a_Normal;\n' +
  'uniform mat4 u_MvpMatrix;\n' +
  'uniform mat4 u_NormalMatrix;\n' +    // Transformation matrix of the normal
  'uniform vec3 u_LightColor;\n' +      // Light color
  'uniform vec3 u_LightDirection;\n' + // Light direction
  'uniform vec3 u_AmbientLight;\n' +    // Ambient light color
  'varying vec4 v_Color;\n' +
  'void main() {\n' +
  '  gl_Position = u_MvpMatrix * a_Position;\n' +
  '  vec3 normal = normalize(vec3(u_NormalMatrix * a_Normal));\n' +
  '  float nDotL = max(dot(u_LightDirection, normal), 0.0);\n' +
  '  vec3 diffuse = u_LightColor * a_Color.rgb * nDotL;\n' +
  '  vec3 ambient = u_AmbientLight * a_Color.rgb;\n' +
  '  v_Color = vec4(diffuse + ambient, a_Color.a);\n' +
  '}\n';
顶点着色器中增加了一个uniform变量用来传送法向矢量的变换矩阵,main()函数中则加入了原法向矢量与变换矩阵的乘法算式。当然,在JavaScript代码中也需要加入相关的操作代码,包括获取uniform变量的位置,根据运动方式计算模型矩阵,然后计算其逆转置矩阵并传入着色器中。
4)点光源下的反射效果:
点光源发出的光,在三维空间的不同位置上方向不同。在对点光源下的物体进行着色时,需要在每个入射点计算点光源发出的光在该处的方向。着色器代码为:
var VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' +
  'attribute vec4 a_Color;\n' +
  'attribute vec4 a_Normal;\n' +
  'uniform mat4 u_MvpMatrix;\n' +
  'uniform mat4 u_ModelMatrix;\n' +     // Model matrix
  'uniform mat4 u_NormalMatrix;\n' +    // Coordinate transformation matrix of the normal
  'uniform vec3 u_LightColor;\n' +      // Light color
  'uniform vec3 u_LightPosition;\n' +   // Position of the light source
  'uniform vec3 u_AmbientLight;\n' +    // Ambient light color
  'varying vec4 v_Color;\n' +
  'void main() {\n' +
  '  gl_Position = u_MvpMatrix * a_Position;\n' +
  '  vec3 normal = normalize(vec3(u_NormalMatrix * a_Normal));\n' +
  '  vec4 vertexPosition = u_ModelMatrix * a_Position;\n' +
  '  vec3 lightDirection = normalize(u_LightPosition - vec3(vertexPosition));\n' +
  '  float nDotL = max(dot(normal, lightDirection), 0.0);\n' +
  '  vec3 diffuse = u_LightColor * a_Color.rgb * nDotL;\n' +
  '  vec3 ambient = u_AmbientLight * a_Color.rgb;\n' +
  '  v_Color = vec4(diffuse + ambient, a_Color.a);\n' +
  '}\n';
顶点着色器代码中,attribute变量未变,还是是顶点坐标、顶点基底颜色、表面法向归一化矢量;增加了uniform变量到6个,分别是视图变换矩阵、模型变换矩阵、法向矢量变换矩阵、入射光颜色、入射光源位置、环境光颜色;还有一个varying变量。main()函数中,使用视图变换矩阵乘以顶点坐标,结果赋值内置顶点变量gl_Position;法向矢量则通过法向矩阵变换并归一化后得到新的法向矢量,而顶点位置经模型矩阵变换到全局坐标系中;光线方向是光源位置与顶点全局坐标的差值并归一化,然后计算光线方向与法向矢量的点积并只取正值,结果与传入的顶点颜色、光源颜色相乘得到漫反射光的颜色;用顶点颜色与传入的环境光相乘得到环境光产生的反射光颜色,将上面两种颜色相加得到的颜色值赋值给varying变量。
因为增加了uniform变量,所以在JavaScript代码中要获取其在缓冲区中的位置以便传入数据,因为使用了点光源,要设置其坐标。
如果只是在顶点着色器中处理,绘出物体的暗部与亮部之间的分界不自然,因为物体颜色只是简单传入了顶点颜色,然后经过内插得到各片元颜色。但点光源照射时,表面上各点的光照效果需要对每个点计算,这需要使用片元着色器。代码为:
var FSHADER_SOURCE =
  '#ifdef GL_ES\n' +
  'precision mediump float;\n' +
  '#endif\n' +
  'uniform vec3 u_LightColor;\n' +      // Light color
  'uniform vec3 u_LightPosition;\n' +   // Position of the light source
  'uniform vec3 u_AmbientLight;\n' +    // Ambient light color
  'varying vec3 v_Normal;\n' +
  'varying vec3 v_Position;\n' +
  'varying vec4 v_Color;\n' +
  'void main() {\n' +
  '  vec3 normal = normalize(v_Normal);\n' +
  '  vec3 lightDirection = normalize(u_LightPosition - v_Position);\n' +
  '  float nDotL = max(dot(lightDirection, normal), 0.0);\n' +
  '  vec3 diffuse = u_LightColor * v_Color.rgb * nDotL;\n' +
  '  vec3 ambient = u_AmbientLight * v_Color.rgb;\n' +
  '  gl_FragColor = vec4(diffuse + ambient, v_Color.a);\n' +
  '}\n';
片元着色器中定义了3个uniform变量,分别是光源颜色、光源位置、环境光颜色;还定义了3个varying变量,分别是片元法向矢量、片元顶点位置和片元顶点颜色。main()函数中,先对片元法向矢量归一化,继而对光源坐标和片元顶点坐标形成的矢量归一化,然后计算光线方向矢量与片元法向矢量的点积,这个结果与光源颜色、片元顶点颜色的乘积得到每个片元的漫射光,片元顶点颜色与环境光的乘积得到片元的环境光,片元漫射光与片元环境光相加的值赋给内部变量gl_FragColor。其中的法向矢量、顶点位置和顶点颜色都是varying变量,是从顶点着色器传过来的值经插值得到的,已经是每个片元的值了。因为使用了很多varying变量,所以顶点着色器代码也需要进行一些更改。
var VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' +
  'attribute vec4 a_Color;\n' +
  'attribute vec4 a_Normal;\n' +
  'uniform mat4 u_MvpMatrix;\n' +
  'uniform mat4 u_ModelMatrix;\n' +    // Model matrix
  'uniform mat4 u_NormalMatrix;\n' +   // Transformation matrix of the normal
  'varying vec4 v_Color;\n' +
  'varying vec3 v_Normal;\n' +
  'varying vec3 v_Position;\n' +
  'void main() {\n' +
  '  gl_Position = u_MvpMatrix * a_Position;\n' +
  '  v_Position = vec3(u_ModelMatrix * a_Position);\n' +
  '  v_Normal = normalize(vec3(u_NormalMatrix * a_Normal));\n' +
  '  v_Color = a_Color;\n' +
  '}\n';
顶点着色器中的代码反而简单一些了,attribute变量有顶点坐标、顶点颜色、法向矢量;uniform变量是3个矩阵,投影矩阵、模型矩阵、法向矢量的变换矩阵;与片元着色器对应,有3个varying变量,分别为片元顶点坐标、片元顶点颜色、片元法向矢量。main()函数中,顶点坐标经投影矩阵变换后赋值给内部变量gl_Position,顶点坐标经模型变换后赋值给片元顶点坐标varying变量,法向矢量经法向矩阵变换并归一化后赋值给片元法向矢量varying变量,顶点颜色直接赋值给片元顶点varying变量。
因为着色器代码做了改变,所以JavaScript代码中也要相应变化,但还是应用了投影矩阵、模型矩阵、法向矢量变换矩阵,只需要很少的变化和调整。
如果场景中有多个点光源,就需要在片元着色器中计算每一个点光源并加上环境光对片元颜色的贡献,并全部加起来。有几个点光源就需要计算几次。
5)转置矩阵的近似方法:
物体运动时,需要求变化后的法向矢量,这时需要使用模型矩阵的逆转置矩阵,这个矩阵与法向矢量的乘积就是移动后的法向矢量。但逆转置矩阵计算相对比较复杂,其实在一些情况下可以用相对简单的计算方法来替代。
当物体只是旋转,就可以使用旋转矩阵直接乘以法向矢量,就能得到旋转后的法向矢量。而在只有平移时,可以从4x4矩阵的左上角抽出3x3的子矩阵,然后乘以法向矢量:
vec3 normal=normalize(mat3(u_ModelMatrix)*a_Nirmal.xyz);
从4x4矩阵的左上角抽出3x3的子矩阵来直接求新法向矢量的方法其实还可以用于旋转和等比例缩放的情况,但缩放时如果在各个方向的缩放因子不同就只能使用逆转置矩阵来求新的法向矢量了。旋转情况下,如果法向矢量已经归一化了,旋转后可以不用再归一化操作,而缩放后则必须再次进行归一化操作。

6. 组合体与层次模型:

实际绘制的图形常常是由多个简单部件组成的复杂模型,绘制时需要处理模型的整体移动,以及各个小部件间的相对移动。
1)层次模型:
对复杂模型,最常用的方法是按照模型中各个部件的层次顺序,从高到低逐一绘制,并在每个部件上应用模型矩阵。比如一个模型,有一个基座,上面有固定在基座上的旋转部件,旋转部件上还有两个能左右移动的部件。绘制时,就需要按照层面模型,先绘制底座,再绘制其上面的旋转部件,最后绘制两个移动的小部件。因为下层的基座的位置变动会影响固定在其上的其他部件,需要先绘制;确定其新的位置后才能绘制其上的转动部件;转动部件位置确定后,依据其新的坐标最后绘制上面的两个移动小部件。
但两个移动小部件不再是层次关系,而是兄弟关系,所以在绘制这两个部件前,需要将相关的参数及模型矩阵等保存起来,绘制其中一个小部件后,再将保存的参数及模型矩阵取出,然后绘制另一个小部件。可以使用一种栈,将前面的参数及矩阵压栈,需要时再从栈中弹出。只要栈足够深,使用这种方法就可以绘制任意复杂的层次结构模型。
使用层次模型方法,只需要定义一组顶点数据,就可以实现复杂组件的绘制。
2)使用多个缓冲区:
也可以把每个部件都定义一组顶点数据,并存储在一个单独的缓冲区中。通常,一个部件的顶点数据包括顶点坐标、法向矢量、索引值等,如果每个部件都是一类物体,比如立方体,可以让各个部件共享法向矢量和索引值,而只是各个部件的顶点坐标分别存储到对应的缓冲区中。
每个部件的顶点坐标存储在对应的缓冲区中,绘制部件之前将相应缓冲区对象分配给a_Position变量,开启a_Position变量并绘制该部件。
JavaScript可以为对象添加新属性,并为添加的属性赋值,比如为缓冲区对象添加type属性保存数据类型、添加num属性保存顶点的个数,程序中使用不同的缓冲区时就能从缓冲区对象的属性中获得type和num的对应值,然后进行相应的处理。

7. 与用户的交互操作:

1)鼠标旋转:
使用鼠标旋转物体时,在鼠标左键按下时记录鼠标的初始坐标,然后在鼠标移动时用当前坐标减去初始坐标以获得鼠标的位移,然后根据这个位移来计算旋转矩阵。
实现时需要加入鼠标移动事件的监听,并在事件响应函数中计算鼠标的位移、旋转矩阵,从而旋转物体。
2)鼠标选中物体:
有时候需要允许用户能够交互地操作三维物体,首先要选中物体。选中三维物体比二维物体要复杂,因为需要较多的数学过程来计算鼠标是否悬浮在某个图形上。
为了检查鼠标是否点中物体,当鼠标左键按下时,将整个立方体重绘为单一颜色(如红色),读取鼠标点击处的像素颜色,然后使用立方体原来的颜色对其重绘。如果前面读到的颜色是红色,就显示提示消息。
为了避免用户看不到重绘红色的过程,需要在取出像素颜色后立即(一帧之内)将立方体重绘成原样,否则就会看到闪烁。
对于具有多个物体的场景,需要为场景中的每个物体指定不同的颜色。如果三维模型过于复杂,或者绘图区域较大,这种方法会很繁琐,也可以使用简化模型,或缩小绘图区域,或使用帧缓冲区对象。
3)选中一个面:
可以使用类似上述方法选中一个面。首先在顶点着色器中添加attribute属性a_Face,表示立方体各表面的编号,即当前顶点属于哪个面。当鼠标点击时,这个值就被编码为颜色值的alpha分量。当某个表面被选中时,就通过u_PickedFace变量来通知着色器这个表面被选中,顶点着色器就可以将这个表面绘制成某个特定颜色,用户就得到了反馈,知道哪个面被选择了。
正常情况下,顶点着色器会比较当前被选中的表面编号和当前顶点的编号,如果它们相等,即当前顶点属于被选中的表面。

8. WebGL的一些特效:

1)在三维图形上叠加提示信息:
可以在网页上叠加两个canvas,使用绝对位置使二者重合,其中一个canvas显示WebGL三维图形,而上面的另一个则显示提示信息等二维文字或图形。
2)网页上显示三维物体:
也需要使用绝对定位的canvas来显示三维图形,并需要设置其背景色为透明,这样canvas下面的网页就可以被看到了。如果canvas的背景色设为0~1之间的值,则会变为半透明。
3)雾化:
实现雾化有多种方法,最简单的是线性雾化。线性雾化中,某一点的雾化程度取决于它与视点间距离,距离越远雾化程度越高。线性雾化有起点和终点,起点表示开始雾化处,终点表示完全雾化处,两点之间某一点的雾化程度与该点与视点的距离呈线性关系。比终点更远的点完全雾化了,看不见了。
某一点的雾化程度定义为雾化因子fog factor:
雾化因子=(终点 - 当前点与视点的距离)/(终点 - 起点)
雾化因子为1.0,表示该点完全没有雾化,可以清晰看到此物体;如果雾化因子为0.0,就完全雾化了,物体完全看不见。
在片元着色器中根据雾化因子计算片元的颜色:
片元颜色 = 物体颜色 x 雾化因子 + 雾的颜色 x (1 - 雾化因子)
OpenGL中常用指数雾化。使用其他雾化方法,只需要更改着色器中的雾化指数。
因为计算每个顶点与视点之间的距离会造成较大开销,可能影响性能,可以使用顶点坐标经过模型视图投影矩阵变换后得到的w分量。顶点着色器中,gl_Position内置变量的w分量正是z分量值乘以-1,可以直接使用该值来近似顶点与视点的距离。
4)绘制圆点:
在WebGL中绘制的点是方的,这样更简单。如果想绘制圆点,要将绘制的方点削成圆的。
片元着色器中,使用内置变量gl_FragCoord来访问片元的坐标,这其实是片元窗口的坐标,片元窗口中包括多个片元。片元着色器还有另一个内置变量gl_PointCoord,是片元窗口中每个片元的坐标,坐标值是从0.0到1.0。要把方形削成圆形,需要将与点中心(0.5, 0.5)距离超过0.5的片元剔除,使用discard语句。
5)α混合与半透明的三维物体:
颜色中的ɑ分量控制颜色的透明度。如果一个物体颜色的ɑ分量为0.5,该物体就是半透明的,透过它可以看到后面的物体。如果一个物体颜色的ɑ分量为0,那么就是完全透明的,将完全看不到它。
要实现ɑ混合,需要使用gl.enable(gl.BLEND来开启混合功能,并指定混合函数gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)。
开启ɑ混合,实际上WebGL用到了两个颜色,即源颜色和目标颜色,前者是待混合进去的颜色,后者是待被混合进去的颜色。混合后的颜色为源颜色乘以其透明度,加上目标颜色乘以1减去源颜色的透明度。
如果绘制的三维场景中既有不透明物体,也有半透明物体,就要按以下方式绘制图形:
⑴开启隐藏面消除功能:gl.enable(gl.DEPTH_TEST);
⑵绘制所有不透明的物体。
⑶锁定用于进行隐藏面消除的深度缓冲区的写入操作,使之只读:gl.depthMask(false);
⑷绘制所有半透明的物体,注意它们要按深度排序,然后从后向前绘制。
⑸释放深度缓冲区,使之可读可写:gl.depthMask(true);
深度缓冲区存储了每个像素的z坐标值,是归一化的0.0~1.0之间。如果场景中有前后重叠的两个三角形A和B,绘制A时将其每个片元的z值写入深度缓冲区,在绘制B时,将B中与A重叠的片元和深度缓冲区中对应像素的z值比较:如深度缓冲区中的z值小,说明三角形A在前,那么B的这个片元就被舍弃;如果深度缓冲区中的z值大,说明B在前面,就把B的这个片元写入颜色缓冲区中,将之前的A的颜色覆盖。这样,绘制完成后,颜色缓冲区中的所有像素都是最前面的片元了,而且每个像素的z值都存储在深度缓冲区中。这个过程就是隐藏面消除的原理。上面的操作是在片元层面上进行的。

9. WebGL中使用帧缓冲区及使用多个着色器:

WebGL中绘制图形一般是使用颜色缓冲区,颜色缓冲区中的图形经着色器处理后会直接显示在canvas画布上。但有时候,也可以使用帧缓冲区,写入帧缓冲区的数据并不会直接显示,往往是用来做中间图形,比如使用为纹理或显示阴影等。复杂的场景中,对不同的物体经常需要使用不同的着色器来绘制,每个着色器可能有很复杂的逻辑以实现不同的效果。准备多个着色器,根据需要来切换使用。
1)渲染到纹理:
使用WebGL渲染三维图形,然后将渲染结果作为纹理贴到另一个三维物体上。把渲染结果作为纹理,就是动态生成图像,而不是向服务器请求加载外部图像。在纹理图像被贴上之前,还可以对其预处理,比如动态模糊或景深效果。
WebGL绘图,一般是使用颜色缓冲区,但也可以使用帧缓冲区framebuffer。绘制在帧缓冲区中的图像并不会直接显示在canvas上,可以先对帧缓冲区中的内容进行一些处理再显示,或者直接使用其中的内容作为纹理图像。在帧缓冲区中的绘制称为离屏绘制offscreen drawing。
一个帧缓冲区有3个关联对象,颜色关联对象color attachment、深度关联对象depth attachment和模板关联对象stencil attachment,分别用来替代颜色缓冲区、深度缓冲区和模板缓冲区。每个关联对象又可以是纹理对象或渲染缓冲区对象两种类型。当把纹理对象作为颜色关联对象关联到帧缓冲区后,WebGL就可以在纹理对象中绘图;渲染缓冲区对象是一种更加通用的绘图区域,可以向其中写入多种类型的数据。
如果希望把WebGL渲染出的图像作为纹理,那么就需要将纹理对象作为颜色关联对象关联到帧缓冲区上,然后在帧缓冲区中进行绘制,此时颜色关联对象就替代了颜色缓冲区,还需要创建帧缓冲区的深度关联对象来替代深度缓冲区。
实现上述配置需要的步骤为:
·创建帧缓冲区对象:gl.createFramebffer()
·创建纹理对象并设置其尺寸和参数:gl.createTexture() gl.bindTexture() gl.rexImage2D() gl.Parameteri()
·创建渲染缓冲区对象:gl.createRenderbuffer()
·绑定渲染缓冲区对象并设置其尺寸:gl.bindRenderbuffer() gl.renderbufferStorage()
·将帧缓冲区的颜色关联对象指定为一个纹理对象:gl.frambufferTexture2D()
·将帧缓冲区的深度关联对象指定为一个渲染缓冲区对象:gl.framebufferRenderbuffer()
·检查帧缓冲区是否正确配置:gl.checkFramebufferStatus()
·在帧缓冲区中进行绘制:gl.bindFramebuffer()
2)绘制阴影:
阴影是物体在光照下向背光处投下影子的现象。实现阴影有多种方法,常用的有阴影贴图shadow map,或称深度贴图depth map。
这种方法中,需要使用两对着色器,一对着色器用来计算光源到物体的距离,另一对着色器根据计算出的距离绘制场景。使用一张纹理图像把前一对着色器的结果传入后一对着色器中,这张纹理图像就称为阴影贴图,通过阴影贴图实现阴影的方法称为阴影映射。步骤为:
①将视点移到光源位置处,运行一对着色器,这个着色器中要绘出的片元都是被光照射到的,即落在这个像素的各个片元中最前面。并不实际绘制出片元的颜色,而只是将片元的z值写入到阴影贴图中。
②将视点移回原来的位置,运行第2对着色器绘制场景。此时计算出每个片元在光源坐标系下的坐标,并与阴影贴图中记录的z值比较,如果前者大于后者,就说明当前片元在阴影中,用较暗的颜色绘制。
两个步骤使用不同的着色器,需要切换着色器。第1步中使用着色器负责生成阴影贴图,顶点着色器将顶点坐标与模型视图投影矩阵变换,片元着色器将片元的z值写入纹理贴图中,其中使用了内置变量gl_FragCoord。
var SHADOW_VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' +
  'uniform mat4 u_MvpMatrix;\n' +
  'void main() {\n' +
  '  gl_Position = u_MvpMatrix * a_Position;\n' +
  '}\n';
var SHADOW_FSHADER_SOURCE =
  'precision mediump float;\n' +
  'void main() {\n' +
  '  gl_FragColor = vec4(gl_FragCoord.z, 0.0, 0.0, 0.0);\n' + // Write the z-value in R
  '}\n';
绘制时将目标切换到帧缓冲区,把视点在光源处的模型视图投影矩阵传给着色器变量,运行着色器,着色器会将每个片元的z值写入帧缓冲区关联的阴影贴图中。gl_FragCoord是vec4类型,gl_FragCoord.x和gl_FragCoord.y是片元在屏幕上的坐标,而gl_FragCoord.z是归一化到(0.0, 1.0)的深度值,这个值写到阴影贴图的R分量中,其实也可以使用其他分量。这样,着色器就将视点位于光源时每个片元的z值存储在阴影贴图中,这个阴影贴图将作为纹理对象传给另一对着色器中的变量中。
第2步中,视点移回原位,开始绘制场景,绘制目标切换回颜色缓冲区。此时顶点着色器中要传入此时的模型视图投影矩阵u_MvpMatrix,还有视点位于光源处的模型视图投影矩阵u_MvpMatrixFromLight;而v_PositionFromLight变量是顶点在光源坐标系中的坐标,这是一个varying变量,需要传入片元着色器。
var VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' +
  'attribute vec4 a_Color;\n' +
  'uniform mat4 u_MvpMatrix;\n' +
  'uniform mat4 u_MvpMatrixFromLight;\n' +
  'varying vec4 v_PositionFromLight;\n' +
  'varying vec4 v_Color;\n' +
  'void main() {\n' +
  '  gl_Position = u_MvpMatrix * a_Position;\n' +
  '  v_PositionFromLight = u_MvpMatrixFromLight * a_Position;\n' +
  '  v_Color = a_Color;\n' +
  '}\n';
片元着色器中,根据片元在光源坐标系中的坐标计算出可以与阴影贴图比较的z值,为了要与前一步获得的z值比较,需要使用与阴影贴图相同的方法来进行归一化。为了将z值与阴影贴图中的相应纹理像素比较,需要通过v_PositionFromLight的x、y坐标从纹理贴图中获取纹理像素。由于WebGL中的x、y坐标在[-1.0, 1.0]区间,而纹理坐标在[0.0, 1.0]区间,需要将x、y坐标进行转换,使用公式s=(x/w)/2.0+0.5; t=(y/w)/2.0+0.5。
var FSHADER_SOURCE =
  'precision mediump float;\n' +
  'uniform sampler2D u_ShadowMap;\n' +
  'varying vec4 v_PositionFromLight;\n' +
  'varying vec4 v_Color;\n' +
  'void main() {\n' +
  '  vec3 shadowCoord = (v_PositionFromLight.xyz/v_PositionFromLight.w)/2.0 + 0.5;\n' +
  '  vec4 rgbaDepth = texture2D(u_ShadowMap, shadowCoord.xy);\n' +
  '  float depth = rgbaDepth.r;\n' + // Retrieve the z-value from R
  '  float visibility = (shadowCoord.z > depth + 0.005) ? 0.7 : 1.0;\n' +
  '  gl_FragColor = vec4(v_Color.rgb * visibility, v_Color.a);\n' +
  '}\n';
代码中比较shadowCoord.z和depth进行比较来决定是否在阴影中,如果前者较大说明当前片元在阴影中,visibility赋值为0.7,否则赋值1.0。比较时加了一个0.005偏移量,如果没有这个偏移量可能出现马赫带Mach band。三维图形学经常出现马赫带,主要原因是纹理图像的RGBA分量都是8位,存储在阴影贴图中的z值精度也只有8位,但与阴影贴图比较的值shadowCoord.z是float类型,为16位。在进行比较时,因为精度不够会造成某些区域会误认为是阴影。为了避免这个精度造成的问题,为阴影贴图添加了一个偏移量0.005,这个偏移量要略大于精度1/256。
但有时,光源远时,gl.FragCoord.z的值会增加,当数值超过8位的R分量可以表示的范围后,阴影会消失。为了解决这个问题,使用阴影贴图中的R、G、B、A这4个分量用4字节32位来存储。

10. 使用三维建模软件生成的图形文件:

实际使用中,大部分三维程序都是从模型文件中读取三维模型的顶点坐标和颜色数据,而模型文件是由三维建模软件生成的。
1)加载三维模型:
Blender是一款很流行的建模软件,可以将三维模型导出为OBJ格式。OBJ格式是基于文本的,最初由Wavefront Technologies公司开发,有开放特性,被各三维图形软件商所接受,易于转化为其他格式。Blender软件创建的模型存为OBJ模型文件后,就可以用程序读取。
程序要读取模型文件,需要准备存放顶点数据的Float32Array类型数组、存放顶点颜色数据的Float32Array类型数组、存放法线矢量数据的Float32Array类型数组、存放顶点索引数据的Uint16Array或Uint8Array类型的数组,然后从模型文件中读取相关数据,然后将获取的数据写入缓冲区,调用gl.drawElements()绘制图形。
2)OBJ文件格式:
OBJ文件又若干个部分组成,包括顶点坐标部分、表面定义部分、材质定义部分等,其中分定义了顶点、法线、表面等等。
⑴OBJ文件格式示例:
# Blender v2.60 (sub 0) OBJ File: ''
# www.blender.org
mtllib cube.mtl
o Cube
v 1.000000 -1.000000 -1.000000
v 1.000000 -1.000000 1.000000
v -1.000000 -1.000000 1.000000
v -1.000000 -1.000000 -1.000000
v 1.000000 1.000000 -1.000000
v 1.000000 1.000000 1.000001
v -1.000000 1.000000 1.000000
v -1.000000 1.000000 -1.000000
usemtl Material
f 1 2 3 4
f 5 8 7 6
f 2 6 7 3
f 3 7 8 4
f 5 1 4 8
usemtl Material.001
f 1 5 6 2
⑵OBJ文件基本结构说明:
·以#开头的行表示注释,有三维建模软件自动生成
·mtlib xxx.mtl行:表示引用外部材质文件
·o Cube行:指定了模型文件的名称
·后面8行定义了顶点坐标,格式为 v x y z [w],其中w为可选的,没有时默认为1.0
·usemtl Material行:指定了某个材质,然后列举使用这个材质的表面
·使用某个材质的表面的行使用格式f v1 v2 v3 v4 ...,其中v1~v4等为顶点索引,从1开始
·如果包含法线矢量,格式为f v1//vn1 v2//vn2 v3//vn3 ...,其中vn1~vn3等是法线矢量索引
·后面的usemtl Material.001是定义另一种材质的表面,后面列举了使用这个材质的表面
模型文件中并没有直接给出法向矢量,程序中要通过顶点组成的矢量经叉积计算得到。
3)MTL文件格式:
⑴MTL文件示例:
MTL文件定义了多个文件,示例代码:
# Blender MTL File: ''
# Material Count: 2
newmtl Material
Ka 0.000000 0.000000 0.000000
Kd 1.000000 0.000000 0.000000
Ks 0.000000 0.000000 0.000000
Ns 96.078431
Ni 1.000000
d 1.000000
illum 0
newmtl Material.001
Ka 0.000000 0.000000 0.000000
Kd 1.000000 0.450000 0.000000
Ks 0.000000 0.000000 0.000000
Ns 96.078431
Ni 1.000000
d 1.000000
illum 0
⑵MTL文件格式:
·前面是注释行
·newmtl行定义一个新材质,其中的材质名被OBJ文件引用
·后面3行以Ka、Kd、Ks开头,定义了表面的环境色、漫反射和高光色,RGB格式
·使用Ns、Ni、d、illum开头的行指定了高光的权重、表面光学密度、透明度、光照模型
·后面的行定义了另一种材质,也有对应的以Ka、Kd、Ks、Ns、Ni、d、illum开头的行
4)加载模型文件的流程:
首先要为三维模型的顶点坐标、颜色、法向矢量各创建一个空的缓冲区,然后在程序中从OBJ文件中解析出这些数据并写入缓冲区。程序中有自定义的readOBJFile()函数,用于读取OBJ文件的内容,并解析数据进入缓冲区,其中要引用OBJ文件的URL。
readOBJFile()函数中,要先创建一个XMLHttpRequest对象,注册事件响应函数,使用open()方法创建一个请求以加载模型文件,发起请求开始加载模型文件。
为了解析OBJ文件,可以自定义2个类OBJDoc和StringParser,分别用来读取文件及解析读到的字符串,自定义类需要定义其构造方法、初始化方法等,在读取文件或解析数据时使用。

11. 响应上下文丢失:

WebGL使用计算机的图形硬件,而这部分资源是被操作系统管理的,是多个应用程序的共享资源。在某些情况下,如果另一个应用程序接管了图形硬件,或者操作系统进入休眠,浏览器就会失去使用这些资源的权力,并导致存储在硬件中的数据丢失,这时WebGL绘图上下文就会丢失。
当丢失上下文时,隐含了以下事情:
·gl.isContextLost()方法返回true
·声明为void的WebGL方法立刻返回
·把null传递给可以返回null的WebGL方法
·gl.getAttribLocation()方法不是返回顶点着色器中属性的有效位置,而是返回-1
·丢失上下文时,第一次调用gl.getError()方法会返回gl_CONTEXT_LOST_WEBGL信息,之后则返回gl.NO_ERROR,直到上下文恢复为止
·所有以is开头的WebGL方法(如isFinished()、isProgram()等)都返回false
WebGL提供了webglcontextlost和webglcontextrestored两个事件来表示这种情况,其中webglcontextlost在Webgl上下文丢失时触发,而webglcontextrestored在浏览器完成对WebGL系统的重置后触发。为了使用这两个事件,要使用addEventListener()事件监听函数。
在程序中,首先要使用语句创建事件两个监听器:
canvas.addEventListener('webglcontextlost', contextLost, false);
canvas.addEventListener('webglcontextrestored', function(ev){start(canvas);}, false);
在webglcontextrestored事件监听处理中调用了自定义的start(canvas)函数。start()函数中,包括了获取canvas及其上下文context,还有加载编译初始化着色器部分。为了能恢复webglcontextlost发生时的状态,还需要把当时的反映物体状态的变量定义为全局变量,恢复时才能接续进行。
webglcontextlost监听事件中的contextLost,函数则很简单,先使用cancelAnimationFrame()方法停止动画绘制,然后阻止浏览器对该事件的默认处理:
function contextLost(ev){
 cancelAnimationFrame(g_requestID);
 ev.preventDefault();
}
为了使应用程序更加健壮,需要考虑丢失上下文的问题。包括以下以下注意事项:
⑴不要给WebGL资源对象添加属性:
有些程序会给新建的buffer等对象添加自定义属性,但当上下文丢失时gl.createBuffer()返回null,为其添加属性就会出现异常。同样,也不应为纹理对象、着色器对象、程序对象添加自定义属性。可以使用不是由WebGL API创建的全局变量添加属性作为替代,使用一种自定义对象来保存相关信息,这些变量和对象是在浏览器管理中,不受WebGL上下文丢失影响。
⑵在检查着色器编译结果的同时检查丢失上下文:
在调用gl.getShaderParameter()方法检查着色器的编译结果时,调用gl.isContextLost()方法检查上下文是否丢失,防止丢失上下文导致编译失败:
gl.shaderSource(shader, shaderSource);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)&&(!gl.isContextLost()){
 console.log(gl.getShaderInfoLog(shader));
 return null;
}
return shader;
⑶在检查着色器链接状态的同时检查上下文是否丢失:
var shaderProgram=gl.createProgram();
gl.attachShader(shaderProgram, vertextShader);
gl.attachShader(shaderProgram, fragmentShader);
gl.linkProgram(shaderProgram);
if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)&&!gl.isContextLost()){
 console.log("Failed to setup shader");
}

12. 需要使用一些方法:

1)gl.readPixels(x, y, width, height, format, type, pixels)
从颜色缓冲区(或帧缓冲区)中读取由x、y、width、height参数确定的矩形块中的所有像素值,并保存在pixels指定的数组中。其中,参数x、y指定缓冲区中矩形块左上角的坐标,同时也是读取的第1个像素的坐标;参数width、height指定矩形块的宽度和高度,以像素为单位;参数format指定像素的颜色格式,必须为gl.RGBA;参数type指定像素值的数据格式,必须为gl.UNSIGNED_BYTE;参数pixels指定用来接收像素值数据的Uint8Array类型化数组。如果一个帧缓冲区对象绑定到gl.FRAMEBUFFER上,那么此方法就会读取帧缓冲区的内容。
2)gl.blendFunc(src_factor, dst_factor):混合颜色
通过参数src_factor和dst_factor指定进行混合操作的函数,混合后的颜色计算公式为:
混合后的颜色=源颜色 x src_factor + 目标颜色 x dst_factor
其中,参数src_factor指定源颜色在混合后颜色中的权重因子;参数dst_factor指定目标颜色在混合后颜色中的权重因子。两种权重因子可用的常量为:

常量

R分量的系数

G分量的系数

B分量的系数

gl.ZERO

0.0

0.0

O.0

gl.ONE

1.0

1.0

1.0

gl.SRC_COLOR

Rs

Gs

Bs

gl.ONE_MINUS_SRC_COLOR

(1-Rs)

(1-Gs)

(1-Bs)

gl.DST_COLOR

Rd

Gd

Bd

gl.ONE_MINUS_DST_COLOR

(1-Rd)

(1-Gd)

(1-Bd)

gl.SRC_ALPHA

As

As

As

gl.ONE_MINUS_SRC_ALPHA

(1-As)

(1-As)

(1-As)

gl.DST_ALPHA

Ad

Ad

Ad

gl.ONE_MINUS_DST_ALPHA

(1-Ad)

(1-Ad)

(1-Ad)

gl.SRC_ALPHA_SATURATE

min(As, Ad)

min(As, Ad)

min(As, Ad)

表中,(Rs, Gs, Bs, As)和(Rd, Gd, Bd, Ad)表示源颜色和目标颜色的各个分量。
一种加法混合常用于实现爆炸的光照效果或引人注目的物体:
gl.blendFunc(gl.SRC_ALPHA, gl.ONE)
示例:gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
如果源颜色是半透明的绿色(0.0, 1.0, 0.0, 0.4),目标颜色是普通黄色(1.0, 1.0, 0.0, 1.0),那么src_factor即源颜色的ɑ分量为0.4,而dst_factor则是(1-0.4)=0.6,计算出混合后的颜色是(0.6, 1.0, 0.0)。
3)gl.blendFuncSeparate(srcRGB, dstRGB, srcAlpha, dstAlpha):混合颜色
其中,参数srcRGB和dsrRGB分别指定源和目标的RGB融合因子,参数srcAlpha和dstAlpha分别指定源和目标的alpha融合因子。此方法可以为源和目标的RGB和alpha值分别指定融合函数。融合因子可用的常量为:

常量

RGB融合因子

alpha融合因子

gl.ZERO

(0.0, 0.0, 0.0)

0.0

gl.ONE

(1.0, 1.0, 1.0)

1.0

gl.SRC_COLOR

(Rs, Gs, Bs)

As

gl.ONE_MINUS_SRC_COLOR

(1.0, 1.0, 1.0)-(Rs, Gs, Bs)

(1-As)

gl.DST_COLOR

(Rd, Gd, Bd)

Ad

gl.ONE_MINUS_DST_COLOR

(1.0, 1.0, 1.0)-(Rd, Gd, Bd)

(1-Ad)

gl.SRC_ALPHA

(As, As, As)

As

gl.ONE_MINUS_SRC_ALPHA

(1.0, 1.0, 1.0)-(As, As, As)

(1-As)

gl.DST_ALPHA

(Ad, Ad, Ad)

Ad

gl.ONE_MINUS_DST_ALPHA

(1.0, 1.0, 1.0)-(Ad, Ad, Ad)

(1-Ad)

gl.CONSTANT_COLOR

(Rc, Gc, Bc)

Ac

gl.ONE_MINUS_CONSTANT_COLOR

(1.0, 1.0, 1.0)-(Rc, Gc, Bc)

(1-Ac)

gl.CONSTANT_ALPHA

(Ac, Ac, Ac)

Ac

gl.ONE_MINUS_CONSTANT_ALPHA

(1.0, 1.0, 1.0)-(Ac, Ac, Ac)

(1-Ac)

gl.SRC_ALPHA_SATURATE

(f, f, f)

1

表中,(Rs, Gs, Bs, As)代表源片元的颜色分量,(Rd, Gd, Bd, Ad)表示目标片元的颜色分量,(Rc, Gc, Bc, Ac)表示常量颜色分量,可以使用以下语句指定:
blendColor(red, greed, blue, alpha);
4)gl.depthMask(mack):锁定或释放深度缓冲区的写入操作
其中,参数mask指定是锁定深度缓冲区的写入操作(false),还是释放之(true)。
5)gl.createFramebuffer():创建帧缓冲区对象
无参数,返回值为创建的帧缓冲区对象,创建失败返回null。
6)gl.deleteFramebuffer(framebuffer):删除缓冲区对象
其中,参数framebuffer指定被删除的帧缓冲区对象
7)gl.createRenderbuffer():创建渲染缓冲区对象
无参数,返回值为创建的渲染缓冲区对象,创建失败返回null。
8)gl.deleteRenderbuffer(renderbuffer):删除指定的渲染缓冲区对象
其中,renderbuffer指定被删除的渲染缓冲区对象。
9)gl.bindRenderbuffer(target, renderbuffer):绑定渲染缓冲区
将renderbuffer指定的渲染缓冲区对象绑定到target目标上。如果renderbuffer为null,则将已经绑定在target目标上的渲染缓冲区对象解除绑定。其中,参数target必须为gl.RENDERBUFFER,参数renderbuffer指定绑定的渲染缓冲区。
10)gl.renderbufferStorage(target, internalformat, width, height):创建并初始化渲染缓冲区
其中,target必须为gl.RENDERBUFFER;参数internalformat指定渲染缓冲区中的数据格式;参数width、height指定渲染缓冲区的宽度和高度,以像素为单位。
参数internalformat可以指定的数据格式包括:

gl.DEPTH_COMPONENT16

表示渲染缓冲区将替代深度缓冲区

gl.STENCIL_INDEX8

表示渲染缓冲区将替代模板缓冲区

gl.RGBA4

表示渲染缓冲区替代颜色缓冲区,其中RGBA各占4比特

gl.RGB5_A1

表示渲染缓冲区替代颜色缓冲区,RGB各占5比特,A 1比特

gl.RGB565

表示渲染缓冲区替代颜色缓冲区,RGB分别占5、6、5比特

11)gl.bindFramebuffer(target, framebuffer):将帧缓冲区对象绑定到target目标上
将framebuffer指定的帧缓冲区对象绑定到target目标上。如果framebuffer为null,则将已经绑定在target目标上的帧缓冲区对象解除绑定。其中,参数target必须为gl.FRAMEBUFFER,参数framebuffer指定被绑定的帧缓冲区。
12)gl.framebufferTexture2D(target, attachment, textarget, texture, level):
将texture指定的纹理对象关联到绑定在target目标上的帧缓冲区。其中,参数target必须为gl.FRAMEBUFFER;参数attachment指定关联的类型;参数textarget与textureImage2D()函数的第1个参数相同;参数texture指定关联的纹理对象;参数level指定为0,在使用Mip纹理时指定纹理的层级。
参数attachment可以关联的类型有:

gl.COLOR_ATTACHMENT0

表示texture是颜色关联对象

gl.DEPTH_ATTACHMENT

表示texture是深度关联对象

在WebGL中只能有一个颜色关联对象,所以只有gl.COLOR_ATTACHMENT0。
13)gl.framebufferRenderbuffer(target, attachment, renderbuffertarget, renderbuffer):
将renderbuffer指定的渲染缓冲区对象关联到绑定在target上的帧缓冲区对象。其中,参数target必须为gl.FRAMEBUFFER;参数attachment指定关联的类型;参数renderbuffertarget必须是gl.RENDERBUFFER;参数renderbuffer指定被关联的渲染缓冲区对象。
参数attachment能关联的类型有:

gl.COLOR_ATTACHMENT0

表示renderbuffer是颜色关联对象

gl.DEPTH_ATTACHMENT

表示renderbuffer是深度关联对象

gl.STENCIL_ATTACHMENT

表示renderbuffer是模板关联对象

14)gl.checkFramebufferStatus(target):检查绑定在target上的帧缓冲区对象的配置状态
其中,参数target必须为gl.FRAMEBUFFER。
方法的返回值有:

0

target不是gl.FRAMEBUFFER

gl.FRAMEBUFFER_COMPLETE

帧缓冲区对象已经正确配置

gl.FRAMEBUFFER_INCOMPLETE_ATTACHMENT

某一个关联对象为空,或关联对象不合法

gl.FRAMEBUFFER_INCOMPLETE_DIMENSIONS

颜色关联对象和深度关联对象的尺寸不一致

gl.FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT

帧缓冲区尚未关联任何一个关联对象

15)gl.viewport(x, y, width, heught):设置绘图区域
设置gl.drawArrays()和gl.drawElement()函数的绘图区域,在canvas上绘图时x、y就是canvas中的坐标。其中,参数x、y指定绘图区域的左上角,以像素为单位;参数width、height指定绘图区域的宽度和高度。

 

Copyright@dwenzhao.cn All Rights Reserved   备案号:粤ICP备15026949号
联系邮箱:dwenzhao@163.com  QQ:1608288659