目标:
三个不一样大小的点;三个颜色不一样的点;
在前一章的示例程序中,我们通常会首先穿件一个缓冲区对象,在其中存储顶点的坐标数据,然后将这个缓冲区对象传入顶点着色器。然而,三维图形不仅仅只有顶点坐标信息,还可能有一些其他的信息,包括颜色定点的尺寸(大小)等。比如第3章“绘制和变换三角形”中的示例程序 MultiPoint.js,它绘制了三个单独的点,顶点着色器不仅用刀了顶点的位置信息,还用到了顶点的尺寸信息。在哪个示例中,点的尺寸编码子在着色器,是固定值,而非从外部传入,如下所示:
//顶点着色器程序
var VSHADER_SOURCE =
'attribute vec4 a_Position;'+
'void main(){'+
'gl_Position=a_Position;'+
'gl_PointSize=10.0;'+
'}';
可见,在示例程序中我们将点点的坐标赋值给了 gl_Position。同时将一个固定的、表示点尺寸的数值10.0赋值给了gl_PointSize。现在,如果希望在JS中动态指定点的大小,我们就不仅需要从JS中向着色器传入顶点的坐标信息,还需要传入尺寸信息。
你应该还记得,为了将顶点坐标传入着色器,需要遵循以下几步:
现在,我们希望把多个顶点相关数据通过缓冲区对象传入顶点着色器,其实只需要每种数据重复以上步骤即可。
MultiAttributeSize.js
//顶点着色器程序
var VSHADER_SOURCE =
'attribute vec4 a_Position;'+
'attribute float a_PointSize;'+
'void main(){'+
'gl_Position=a_Position;'+
'gl_PointSize = a_PointSize;'+
'}';
//片元着色器程序
var FSHADER_SOURCE=
'void main(){'+
'gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);'+
'}';
function main() {
//获取canvas元素
var canvas = document.getElementById("webgl");
if(!canvas){
console.log("Failed to retrieve the );
return;
}
//获取WebGL绘图上下文
var gl = getWebGLContext(canvas);
if(!gl){
console.log("Failed to get the rendering context for WebGL");
return;
}
//初始化着色器
if(!initShaders(gl,VSHADER_SOURCE,FSHADER_SOURCE)){
console.log("Failed to initialize shaders.");
return;
}
//设置顶点位置
var n = initVertexBuffers(gl);
if (n < 0) {
console.log('Failed to set the positions of the vertices');
return;
}
//指定清空
gl.clearColor(0.0, 0.0, 0.0, 1.0);
//清空
gl.clear(gl.COLOR_BUFFER_BIT);
//绘制三个点
gl.drawArrays(gl.POINTS, 0, n);
}
function initVertexBuffers(gl) {
var vertices = new Float32Array([
0.0, 0.5, -0.5, -0.5, 0.5, -0.5
]);
var n=3; //点的个数
var sizes = new Float32Array([
10.0, 20.0, 30.0
])
//创建缓冲区对象
var vertexBuffer = gl.createBuffer();
var sizeBuffer = gl.createBuffer();
if(!vertexBuffer){
console.log("Failed to create vertexBuffer object");
return -1;
}
if(!sizeBuffer){
console.log("Failed to create sizeBuffer object");
return -1;
}
//将顶点坐标写入缓存区并开启
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
if(a_Position < 0){
console.log("Failed to get the storage location of a_Position");
return -1;
}
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(a_Position);
//将顶点尺寸写入缓存区并开启
gl.bindBuffer(gl.ARRAY_BUFFER, sizeBuffer);
gl.bufferData(gl.ARRAY_BUFFER, sizes, gl.STATIC_DRAW);
var a_PointSize = gl.getAttribLocation(gl.program, 'a_PointSize');
if(a_PointSize < 0){
console.log("Failed to get the storage location of a_PointSize");
return -1;
}
gl.vertexAttribPointer(a_PointSize, 1, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(a_PointSize);
return n;
}
如你所见,我们添加了一个新的 attribute 变量 a_PointSize,该变量是 float 变量。除此之外,顶点着色器就没有其他变化了。接着,在 JS 中,我们还需要修改 initVertexBuffers()函数来建立多个缓冲区对象,并将其传入着色器。让我们来研究一下。
在 initVertexBuffers()函数中,我们定义了含有顶点坐标数据的数组 vertices,以及设定顶点尺寸数据的数组size。
var sizes = new Float32Array([
10.0, 20.0, 30.0
]);
然后我们创建了两个缓冲区对象 vertexBuffer 和 sizeBuffer,前者用来存储顶点坐标数据,后者用来存储顶点尺寸数据。
接着,绑定存储顶点坐标的缓冲区对象,并向其写入数据,之后分配给 attribute 变量 a_Position 并开启之。这些和之前示例程序中的完全相同。
新加入的部分,将顶点尺寸传入着色器,其步骤与前面相同:绑定缓冲区对象 sizeBuffer,写入顶点尺寸数据,分配给 attribute 变量 a_PointSize并开启之。
一旦 initVertexBuffers()完成了上述两个步骤,WebGL 系统的内部状态就如图所示。可以看到,两个不同的缓冲区对象被分配给了两个不同的 attribute 变量。
这样,WebGL 系统就已经准备就绪了,当执行 gl.drawArrays()函数时,存储在缓冲区对象中的数据将其按照其在缓冲区中的顺序依次传给对应的attribute 变量。在顶点着色器中,我们将这两个 attribute 变量分别赋值给的 gl_Position 和 gl_PointSize,就在指定的位置绘制出指定大小的点了。
可见,通过为点点的每种数据建立一个缓冲区,然后分配给对应的 attribute 变量,你就可以向顶点着色器传递多分逐顶点的数据信息了,如本节中点点尺寸、顶点颜色、顶点纹理坐标、点所在平面的方向量等等。
运行一下:
使用多个缓冲区对象向着着色器传递多种数据,比较适合数据量不大的情况。当程序中的复杂三维图形具有成千上万个顶点时,维护所有的顶点数据是很困难的。想象一下,如果 MultiAttributeSize.js 中的三维模型有 1000个顶点会怎样。然后 WebGL允许我们把顶点的坐标和尺寸数据打包到同一个缓冲区对象中,并通过某种机制分别访问缓冲区对象中不同种类的数据。比如,可将顶点的坐标和尺寸数据按照如下方式交错组织。如下所示:
可见,一旦我们将几种“逐顶点”的数据(坐标和尺寸)交叉存储在一个数组中,并将数组写入一个缓冲区对象。 WebGL就需要有差别地从缓冲区中获取某种特定数据(坐标或尺寸),即使用 gl.vertexAttribPointer()函数的第5个函数stride 和第6个参数 offset。下面看看示例程序。
MultiAttributeSize_Interleaved.js
//顶点着色器程序
var VSHADER_SOURCE =
'attribute vec4 a_Position;'+
'attribute float a_PointSize;'+
'void main(){'+
'gl_Position=a_Position;'+
'gl_PointSize = a_PointSize;'+
'}';
//片元着色器程序
var FSHADER_SOURCE=
'void main(){'+
'gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);'+
'}';
function main() {
//获取canvas元素
var canvas = document.getElementById("webgl");
if(!canvas){
console.log("Failed to retrieve the );
return;
}
//获取WebGL绘图上下文
var gl = getWebGLContext(canvas);
if(!gl){
console.log("Failed to get the rendering context for WebGL");
return;
}
//初始化着色器
if(!initShaders(gl,VSHADER_SOURCE,FSHADER_SOURCE)){
console.log("Failed to initialize shaders.");
return;
}
//设置顶点位置
var n = initVertexBuffers(gl);
if (n < 0) {
console.log('Failed to set the positions of the vertices');
return;
}
//指定清空
gl.clearColor(0.0, 0.0, 0.0, 1.0);
//清空
gl.clear(gl.COLOR_BUFFER_BIT);
//绘制三个点
gl.drawArrays(gl.POINTS, 0, n);
}
function initVertexBuffers(gl) {
var verticesSizes = new Float32Array([
0.0, 0.5, 10.0, //第一个点
-0.5, -0.5, 20.0, //第二个点
0.5, -0.5,30.0 //第三个点
]);
var n=3; //点的个数
//创建缓冲区对象
var vertexSizesBuffer = gl.createBuffer();
if(!vertexSizesBuffer){
console.log("Failed to create vertexSizesBuffer object");
return -1;
}
//将顶点坐标写入缓存区并开启
gl.bindBuffer(gl.ARRAY_BUFFER, vertexSizesBuffer);
gl.bufferData(gl.ARRAY_BUFFER, verticesSizes, gl.STATIC_DRAW);
var FSIZE = verticesSizes.BYTES_PER_ELEMENT;
var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
if(a_Position < 0){
console.log("Failed to get the storage location of a_Position");
return -1;
}
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE*3, 0);
gl.enableVertexAttribArray(a_Position);
var a_PointSize = gl.getAttribLocation(gl.program, 'a_PointSize');
if(a_PointSize < 0){
console.log("Failed to get the storage location of a_PointSize");
return -1;
}
gl.vertexAttribPointer(a_PointSize, 1, gl.FLOAT, false, FSIZE*3, FSIZE*2);
gl.enableVertexAttribArray(a_PointSize);
return n;
}
JS 中 main()函数的基本流程与 MutliAttributeSize.js 相同,只有 initVertexBuffers()被修改了,具体如下。
首先,我们定义了一个类型化数组 verticesSizes,就像在上面的例子中一样。接下来的代码你一定已经很熟悉了:创建缓冲区对象,绑定之,把数据写入缓冲区对象。然后,我们将 vertices 数组中每个元素的大小存储到 FSIZE 中,稍后将会用到它。类型化数组具有 BYTES_PER_ELEMENT 属性,可以从中获知数组中每个元素所占的字节数。
我们需要着手把缓冲区对象分配给 attribute 变量。首先获取 attribute 变量 a_Position的存储地址,方法和之前完全相同,然后调用 gl.vertexAttribPoint()函数。注意,这里的参数设置就与前例有所不同了,因为在缓冲区对象中存储了两种类型的数据:顶点坐标和顶点尺寸。
在第3章中曾提到过 gl.vertexAttribPoint()的函数规范,但是让我们再来看一下其参数 stride 和 offset。
参数 stride 表示,在缓冲区对象中,单个顶点的所有数据的字节数,也就是相邻两个顶点件的距离,即步进参数。
在前面的示例程序中,缓冲区只含有一种数据,即顶点的坐标,所以将其设置为0即可。然后,在本例中,当缓冲区中有了耳朵中昂数据,我们就需要考虑stride的值,如下图所示:
如图所示,每一个顶点有3个数据值(两个坐标数据和一个尺寸数据),因此 stride 应该设置为每项数据大小的三倍,即 3 x FSIZE。
参数offset表示当前考虑的数据项距离收个元素的距离,即偏移参数。在 verticesSizes 数组中,顶点的坐标数据是放在最前面的,所以 offset 应当为0。因此,我们调用 gl.vertexAttribPoint()函数时,如下所示传入 stride 参数和 offset 参数:
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE*3, 0);
gl.enableVertexAttribArray(a_Position);
这样一来,我们就把缓冲区的那么部分顶点坐标数据分配给了着色器中的 attribute 变量 a_Position,并开启了该变量。
接了下来对顶点尺寸数据采取相同的操作:将缓冲区对象中的顶点尺寸数据分配给 a_PointSize。然后在这个例子中,缓冲区对象还是原来那个,只不过这次关注的数据不同,我们需要将 offset 参数设置为顶点尺寸数据在缓冲区对象中的初始位置。在关于某个顶点的三个值中,前两个是顶点坐标,后一个是顶点尺寸,因此 offset 应当设置为 FSIZE*2。我们如下调用 gl.vertexAttribArray()函数,并正确设置 stride 参数和 offset 参数。
gl.vertexAttribPointer(a_PointSize, 1, gl.FLOAT, false, FSIZE*3, FSIZE*2);
gl.enableVertexAttribArray(a_PointSize);
在开启已被分配的缓冲区对象的 a_PositionSize 变量之后,剩下的任务就只有调用 gl.drawArrays()进行绘制操作了。
再次执行顶点着色器时,WebGL 系统会根据 stride 和 offset 参数,从缓冲区中正确地抽出数据,依次赋值给着色器中的各个 attribute 变量,并进行绘制。
最后运算与上一个一样。
现在,我们已经了解将多种顶点数据信息传入顶点着色器的技术,下面就让我们使用这项技术来尝试修改顶点的颜色。具体方法还和之前相同,只不过将顶点尺寸数据改成了颜色数据。我们需要在缓冲区对象中填充顶点坐标与颜色数据,然后分配给 attribute 变量,用以处理颜色。
下面的示例程序将绘制 红色、蓝色、绿色三个点。
你也许还记得在第2章“WebGL入门”中讲过,片元着色器可以用来处理颜色之类的属性。但是到目前为止,我们都只是在片元着色器中静态地设置颜色,还没有真正地研究过片元着色器。虽然现在已经能够将顶点的是颜色数据从 JS 中传给顶点着色器中的 attribute 变量,但是真正能够影响绘制颜色的 gl_FragColor 却在片元着色器中。我们需要知道顶点着色器和片元着色器是如何交流的,这样才能使传入顶点着色器的数据进入片元着色器:
第2章的 ColoredPoints 程序用了一个 uniform 变量来将颜色信息传入片元着色器。然后,因为这是个“一致的”(uniform)变量,而不是“可变的”(varying),我们没法为每个顶点都准备一个值,所以那么程序中的所有顶点都只能是同一个颜色。我们是用一种新的 varying 变量向片元着色器中传入数据,实际上,varying 变量的作用是从顶点着色器向片元着色器传输数据。
MultiAttributeColor.js
//顶点着色器程序
var VSHADER_SOURCE =
'attribute vec4 a_Position;'+
'attribute vec4 a_Color;'+
'varying vec4 v_Color;'+
'void main(){'+
'gl_Position=a_Position;'+
'gl_PointSize = 10.0;'+
'v_Color = a_Color;'+
'}';
//片元着色器程序
var FSHADER_SOURCE=
'precision mediump float;'+
'varying vec4 v_Color;'+
'void main(){'+
'gl_FragColor = v_Color;'+
'}';
function main() {
//获取canvas元素
var canvas = document.getElementById("webgl");
if(!canvas){
console.log("Failed to retrieve the );
return;
}
//获取WebGL绘图上下文
var gl = getWebGLContext(canvas);
if(!gl){
console.log("Failed to get the rendering context for WebGL");
return;
}
//初始化着色器
if(!initShaders(gl,VSHADER_SOURCE,FSHADER_SOURCE)){
console.log("Failed to initialize shaders.");
return;
}
//设置顶点位置
var n = initVertexBuffers(gl);
if (n < 0) {
console.log('Failed to set the positions of the vertices');
return;
}
//指定清空
gl.clearColor(0.0, 0.0, 0.0, 1.0);
//清空
gl.clear(gl.COLOR_BUFFER_BIT);
//绘制三个点
gl.drawArrays(gl.POINTS, 0, n);
}
function initVertexBuffers(gl) {
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 n=3; //点的个数
//创建缓冲区对象
var vertexColorBuffer = gl.createBuffer();
if(!vertexColorBuffer){
console.log("Failed to create vertexColorBuffer object");
return -1;
}
//将顶点坐标写入缓存区并开启
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');
if(a_Position < 0){
console.log("Failed to get the storage location of a_Position");
return -1;
}
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE*5, 0);
gl.enableVertexAttribArray(a_Position);
var a_Color = gl.getAttribLocation(gl.program, 'a_Color');
if(a_Color < 0){
console.log("Failed to get the storage location of a_Color");
return -1;
}
gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, FSIZE*5, FSIZE*2);
gl.enableVertexAttribArray(a_Color);
return n;
}
在顶点着色器中,我们声明了 attribute 变量 a_Color 用以接受颜色数据,然后声明了新的 varying 变量 v_Color,该变量负责将颜色值将被传给片元着色器。注意,varying变量只能是 float (以及相关的 vec2,vec3,vec4,mat2,mat3和mat4)类型的。
'attribute vec4 a_Color;'+
'varying vec4 v_Color;'+
我们将 a_Color 变量的值直接赋给之前声明的 v_Color 变量。
'v_Color = a_Color;'+
那么,片元着色器该如何接受这个变量呢?答案很简单,只需要在片元着色器也声明一个(与顶点着色器中的那么 varying 变量同名)varying 变量就可以了:
'varying vec4 v_Color;'+
在 WebGL 中,如果顶点着色器与片元着色器中类型和命名都相同的varying变量,那么顶点着色器赋给该变量的值就会被自动地传入片元着色器,如图所示:
所以,顶点着色器赋给 v_Color 变量的值被传递给了片元着色器中的 v_Color 变量,然后片元着色器将 v_Color 赋值给 gl_FragColor,这样每个顶点的颜色将被修改。
'gl_FragColor = v_Color;'+
接下来的代码和 MultiAttributeSize.js 相似,唯一的却别在于存储顶点数据的类型化数组的名称被改成了 verticesColors,然后删去顶点尺寸数据再加上顶点颜色数据,比如(1.0, 1.0, 1.0)为红色。
如第2章所述,RGBA 颜色模型中的颜色分量值区间为0.0 到 1.0。就像在 MultiAttributeSize_Stride.js中一样,数组 verticesColor 中有两种不同类型的数组(坐标和颜色)。之前的尺寸只是单个数值,而现在的颜色有3个分量值,所以每个定点所占字节是 FSIZE * 5,需要修改相应的 gl.vertexAttribPointer()函数的 stride 参数和 offset 参数。