目录
[1. 准备用来绘制单色立方体的着色器](#1. 准备用来绘制单色立方体的着色器)
[2. 准备用来绘制纹理立方体的着色器](#2. 准备用来绘制纹理立方体的着色器)
[3. 调用createProgram()函数,利用第1步创建出的着色器,创建着色器程序对象](#3. 调用createProgram()函数,利用第1步创建出的着色器,创建着色器程序对象)
[4. 调用createProgram()函数,利用第2步创建出的着色器,创建着色器程序对象](#4. 调用createProgram()函数,利用第2步创建出的着色器,创建着色器程序对象)
[5. 调用gl.useProgram()函数,指定使用第3步创建出的着色器程序对象。](#5. 调用gl.useProgram()函数,指定使用第3步创建出的着色器程序对象。)
[6. 通过缓冲区对象向着色器中传入attribute变量并开启之。](#6. 通过缓冲区对象向着色器中传入attribute变量并开启之。)
[7. 绘制单色立方体](#7. 绘制单色立方体)
[8. 调用gl.useProgram()函数,指定使用第4步创建出的着色器程序对象。](#8. 调用gl.useProgram()函数,指定使用第4步创建出的着色器程序对象。)
[9. 通过缓冲区对象向着色器传入attribute变量并开启之。](#9. 通过缓冲区对象向着色器传入attribute变量并开启之。)
[10. 绘制纹理立方体](#10. 绘制纹理立方体)
前言
WebGL中,如果一个着色器就能绘制出场景中所有的物体,那就没有问题。然而事实是,对不同的物体经常需要使用不同的着色器来绘制,每个着色器中可能有非常复杂的逻辑以实现各种不同的效果。我们可以准备多个着色器,然后根据需要来切换使用它们。本文的示例程序ProgramObject就使用了两个着色器绘制了两个立方体,一个是纯色的,另一个贴有纹理。下图显示了程序的运行效果。
该程序也可以帮你复习一下如何在物体表面贴上纹理。
如何实现切换着色器
为了切换着色器,需要先创建多个着色器程序对象,然后在进行绘制前选择使用的程序对象。我们使用gl.useProgram() 函数来进行切换。由于现在需要显式地操作着色器和程序对象,所以不能再使用initShaders() 函数了。但是,可以使用定义在cuon-utils.js 中的createProgram ()函数,实际上initShaders()函数内部也是调用该函数来创建着色器对象的。
下面是示例程序的流程步骤,由于它创建了两个程序对象,做了两轮相同的操作,所以看上去有点长。关键的代码实际上很简单。
1. 准备用来绘制单色立方体的着色器
2. 准备用来绘制纹理立方体的着色器
3. 调用createProgram()函数,利用第1步创建出的着色器,创建着色器程序对象
4. 调用createProgram()函数,利用第2步创建出的着色器,创建着色器程序对象
5. 调用gl.useProgram()函数,指定使用第3步创建出的着色器程序对象。
6. 通过缓冲区对象向着色器中传入attribute变量并开启之。
7. 绘制单色立方体
8. 调用gl.useProgram()函数,指定使用第4步创建出的着色器程序对象。
9. 通过缓冲区对象向着色器传入attribute变量并开启之。
10. 绘制纹理立方体
下面看一下示例程序
示例程序(ProgramObject.js)
如下显示了示例程序中的上述第1步到第4步。我们准备了顶点着色器和片元着色器各两种:SOLID_VSHADER_SOURCE(第1行),SOLID_FSHADER_SOURCE(第15行),TEXTURE_VSHADER_SOURCE(第24行),TEXTURE_FSHADER_SOURCE(第39行)。前两者用来绘制单色的立方体,而后两者绘制贴有纹理的立方体。由于本文的重点是如何切换着色器程序对象,所以着色器的具体内容被省略了。
示例效果
示例代码
javascript
var SOLID_VSHADER_SOURCE = // 单色立方体 顶点着色器
'attribute vec4 a_Position;\n' +
'attribute vec4 a_Normal;\n' +
'uniform mat4 u_MvpMatrix;\n' +
'uniform mat4 u_NormalMatrix;\n' +
'varying vec4 v_Color;\n' +
'void main() {\n' +
' vec3 lightDirection = vec3(0.0, 0.0, 1.0);\n' + // 光线方向
' vec4 color = vec4(0.0, 1.0, 1.0, 1.0);\n' + // 表面颜色
' gl_Position = u_MvpMatrix * a_Position;\n' +
' vec3 normal = normalize(vec3(u_NormalMatrix * a_Normal));\n' + // 归一化模型矩阵变换后的法向量
' float nDotL = max(dot(normal, lightDirection), 0.0);\n' + // 点积 (光线/法向量 -> cosΘ)
' v_Color = vec4(color.rgb * nDotL, color.a);\n' +
'}\n';
var SOLID_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';
var TEXTURE_VSHADER_SOURCE = // 纹理立方体 顶点着色器
'attribute vec4 a_Position;\n' +
'attribute vec4 a_Normal;\n' +
'attribute vec2 a_TexCoord;\n' +
'uniform mat4 u_MvpMatrix;\n' +
'uniform mat4 u_NormalMatrix;\n' +
'varying float v_NdotL;\n' +
'varying vec2 v_TexCoord;\n' +
'void main() {\n' +
' vec3 lightDirection = vec3(0.0, 0.0, 1.0);\n' + // 光线方向
' gl_Position = u_MvpMatrix * a_Position;\n' +
' vec3 normal = normalize(vec3(u_NormalMatrix * a_Normal));\n' +
' v_NdotL = max(dot(normal, lightDirection), 0.0);\n' +
' v_TexCoord = a_TexCoord;\n' +
'}\n';
var TEXTURE_FSHADER_SOURCE = // 纹理立方体 片元着色器
'#ifdef GL_ES\n' +
'precision mediump float;\n' +
'#endif\n' +
'uniform sampler2D u_Sampler;\n' +
'varying vec2 v_TexCoord;\n' +
'varying float v_NdotL;\n' +
'void main() {\n' +
' vec4 color = texture2D(u_Sampler, v_TexCoord);\n' +
' gl_FragColor = vec4(color.rgb * v_NdotL, color.a);\n' +
'}\n';
function main() {
var canvas = document.getElementById('webgl');
var gl = getWebGLContext(canvas);
var solidProgram = createProgram(gl, SOLID_VSHADER_SOURCE, SOLID_FSHADER_SOURCE);
var texProgram = createProgram(gl, TEXTURE_VSHADER_SOURCE, TEXTURE_FSHADER_SOURCE);
// 获取单色绘图程序对象中属性和统一变量的存储位置
solidProgram.a_Position = gl.getAttribLocation(solidProgram, 'a_Position');
solidProgram.a_Normal = gl.getAttribLocation(solidProgram, 'a_Normal');
solidProgram.u_MvpMatrix = gl.getUniformLocation(solidProgram, 'u_MvpMatrix');
solidProgram.u_NormalMatrix = gl.getUniformLocation(solidProgram, 'u_NormalMatrix');
// 获取纹理绘制程序对象中属性和统一变量的存储位置
texProgram.a_Position = gl.getAttribLocation(texProgram, 'a_Position');
texProgram.a_Normal = gl.getAttribLocation(texProgram, 'a_Normal');
texProgram.a_TexCoord = gl.getAttribLocation(texProgram, 'a_TexCoord');
texProgram.u_MvpMatrix = gl.getUniformLocation(texProgram, 'u_MvpMatrix');
texProgram.u_NormalMatrix = gl.getUniformLocation(texProgram, 'u_NormalMatrix');
texProgram.u_Sampler = gl.getUniformLocation(texProgram, 'u_Sampler');
// 设置顶点信息
var cube = initVertexBuffers(gl);
// 设置纹理
var texture = initTextures(gl, texProgram);
// 设置透明颜色并启用深度测试
gl.enable(gl.DEPTH_TEST);
gl.clearColor(0.0, 0.0, 0.0, 1.0);
var viewProjMatrix = new Matrix4();
viewProjMatrix.setPerspective(30.0, canvas.width / canvas.height, 1.0, 100.0);
viewProjMatrix.lookAt(0.0, 0.0, 15.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);
// 开始绘制
var currentAngle = 0.0; // 当前旋转角度(度)
var tick = function () {
currentAngle = animate(currentAngle); // 更新当前旋转角度
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// 用单色绘制立方体
drawSolidCube(gl, solidProgram, cube, -2.0, currentAngle, viewProjMatrix);
// 绘制具有纹理的立方体
drawTexCube(gl, texProgram, cube, texture, 2.0, currentAngle, viewProjMatrix);
window.requestAnimationFrame(tick, canvas);
};
tick();
}
function initVertexBuffers(gl) {
// v6----- v5
// /| /|
// v1------v0|
// | | | |
// | |v7---|-|v4
// |/ |/
// v2------v3
var vertices = new Float32Array([ // 顶点坐标
1.0, 1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, // v0-v1-v2-v3 front
1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, 1.0, 1.0, -1.0, // v0-v3-v4-v5 right
1.0, 1.0, 1.0, 1.0, 1.0, -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, 1.0, // v0-v5-v6-v1 up
-1.0, 1.0, 1.0, -1.0, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, 1.0, // v1-v6-v7-v2 left
-1.0, -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, -1.0, 1.0, -1.0, -1.0, 1.0, // v7-v4-v3-v2 down
1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0 // v4-v7-v6-v5 back
]);
var normals = new Float32Array([ // 法向量
0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, // v0-v1-v2-v3 front
1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, // v0-v3-v4-v5 right
0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, // v0-v5-v6-v1 up
-1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, // v1-v6-v7-v2 left
0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, // v7-v4-v3-v2 down
0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0 // v4-v7-v6-v5 back
]);
var texCoords = new Float32Array([ // 纹理坐标
1.0, 1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, // v0-v1-v2-v3 front
0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, // v0-v3-v4-v5 right
1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 0.0, 0.0, // v0-v5-v6-v1 up
1.0, 1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, // v1-v6-v7-v2 left
0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, // v7-v4-v3-v2 down
0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0 // v4-v7-v6-v5 back
]);
var indices = new Uint8Array([ // 顶点索引
0, 1, 2, 0, 2, 3, // front
4, 5, 6, 4, 6, 7, // right
8, 9, 10, 8, 10, 11, // up
12, 13, 14, 12, 14, 15, // left
16, 17, 18, 16, 18, 19, // down
20, 21, 22, 20, 22, 23 // back
]);
var o = new Object(); // 使用该对象返回多个缓冲区对象
// 将顶点信息写入缓冲区对象
o.vertexBuffer = initArrayBufferForLaterUse(gl, vertices, 3, gl.FLOAT);
o.normalBuffer = initArrayBufferForLaterUse(gl, normals, 3, gl.FLOAT);
o.texCoordBuffer = initArrayBufferForLaterUse(gl, texCoords, 2, gl.FLOAT);
o.indexBuffer = initElementArrayBufferForLaterUse(gl, indices, gl.UNSIGNED_BYTE);
o.numIndices = indices.length;
return o;
}
function initTextures(gl, program) {
var texture = gl.createTexture(); // 创建一个纹理对象
var image = new Image();
image.onload = function () {
/* 将图像数据写入纹理对象 */
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1); // 翻转图像Y坐标
gl.activeTexture(gl.TEXTURE0); // 激活0号纹理单元
gl.bindTexture(gl.TEXTURE_2D, texture); // 将纹理对象绑定至0号纹理单元并指定2d纹理类型
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); // 配置纹理参数
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image); // 配置图像参数
/* 将纹理单元0传递到uSampler */
gl.useProgram(program);
gl.uniform1i(program.u_Sampler, 0);
};
image.src = '../resources/orange.jpg';
return texture;
}
function drawSolidCube(gl, program, o, x, angle, viewProjMatrix) {
gl.useProgram(program); // 告诉WebGL使用这个程序对象
/* 分配缓冲区对象并启用分配(非索引) */
initAttributeVariable(gl, program.a_Position, o.vertexBuffer); // 顶点坐标
initAttributeVariable(gl, program.a_Normal, o.normalBuffer); // 法向量
drawCube(gl, program, o, x, angle, viewProjMatrix); // Draw
}
function drawTexCube(gl, program, o, texture, x, angle, viewProjMatrix) {
gl.useProgram(program); // 告诉WebGL使用这个程序对象
/* 分配缓冲区对象并启用分配(非索引) */
initAttributeVariable(gl, program.a_Position, o.vertexBuffer); // 顶点坐标
initAttributeVariable(gl, program.a_Normal, o.normalBuffer); // 法向量
initAttributeVariable(gl, program.a_TexCoord, o.texCoordBuffer);// 纹理坐标
/* 将纹理对象绑定到纹理单元0 */
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture);
drawCube(gl, program, o, x, angle, viewProjMatrix); // Draw
}
function initAttributeVariable(gl, a_attribute, buffer) { // 分配缓冲区对象并启用分配
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.vertexAttribPointer(a_attribute, buffer.num, buffer.type, false, 0, 0);
gl.enableVertexAttribArray(a_attribute);
}
var g_modelMatrix = new Matrix4();
var g_mvpMatrix = new Matrix4();
var g_normalMatrix = new Matrix4();
function drawCube(gl, program, o, x, angle, viewProjMatrix) { // 最终画
/* 计算模型矩阵 */
g_modelMatrix.setTranslate(x, 0.0, 0.0);
g_modelMatrix.rotate(20.0, 1.0, 0.0, 0.0);
g_modelMatrix.rotate(angle, 0.0, 1.0, 0.0);
/* 计算法线变换矩阵 */
g_normalMatrix.setInverseOf(g_modelMatrix);
g_normalMatrix.transpose();
gl.uniformMatrix4fv(program.u_NormalMatrix, false, g_normalMatrix.elements);
/* 计算模型视图投影矩阵 */
g_mvpMatrix.set(viewProjMatrix); // g_mvpMatrix -> viewProjMatrix
g_mvpMatrix.multiply(g_modelMatrix); // viewProjMatrix * g_modelMatrix
gl.uniformMatrix4fv(program.u_MvpMatrix, false, g_mvpMatrix.elements);
gl.drawElements(gl.TRIANGLES, o.numIndices, o.indexBuffer.type, 0); // Draw
}
function initArrayBufferForLaterUse(gl, data, num, type) { // 坐标、法线、纹理坐标专用
var buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
// 保留以后分配给属性变量所需的信息
buffer.num = num;
buffer.type = type;
return buffer;
}
function initElementArrayBufferForLaterUse(gl, data, type) { // 索引数据专用
var buffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, data, gl.STATIC_DRAW);
buffer.type = type;
return buffer;
}
var ANGLE_STEP = 30; // 旋转角度的增量(度)
var last = Date.now(); // 上次调用此函数的时间
function animate(angle) {
var now = Date.now();
var elapsed = now - last;
last = now;
var newAngle = angle + (ANGLE_STEP * elapsed) / 1000.0; // 更新当前旋转角度(根据经过的时间进行调整)
return newAngle % 360;
}
代码详解
main()函数首先调用gl.createProgram()创建了两个着色器程序对象(第54、55行),该函数接收的参数和initShaders()一样,即字符串形式的顶点着色器和片元着色器代码,返回值就是着色器程序对象。两个着色器程序对象分别命名为solidProgram和texProgram。然后,获取每个着色器中各attribute变量的存储地址,保存在相应着色器程序对象的同名属性上。我们又一次用到了JavaScript的"可以随意向对象添加属性"的特性。
接着,将顶点的数据存储在由initVertexBuffers()函数创建的缓冲区对象中。对单色立方体而言,顶点的数据包括(1)顶点的坐标,(2)法线,(3)索引。对贴有纹理的立方体而言,还得加上纹理坐标。这些缓冲区对象将在绘制立方体和切换着色器时分配给着色器中的attribute变量。
具体的,initVertexBuffers()函数首先定义了顶点坐标数组(第102行)、法线数组(第110行)、纹理坐标数组(第118行)和顶点索引数组(第126行),然后定义了一个空的Object类型的对象o,将创建的各个缓冲区对象全部添加为o的属性(第137~141行),最后返回o对象。你也通过向全局变量赋值的方式来传出缓冲区对象,但是全局变量就太多了,程序的可读性也会降低。利用函数返回对象的属性来返回多个缓冲区对象,可以帮助我们更好地管理这些缓冲区对象。
我们使用initArrayBufferForLaterUse()函数来创建单个缓冲区对象(第137~139行),它将数组中的数据写入缓冲区,但不将缓冲区分配给attribute变量。
回到main()函数,我们接着调用initTextures()函数建立好纹理图像(第72行),然后一切就准备好了,只等绘制两个立方体对象。首先调用drawSolidCube()函数绘制单色的立方体(第86行),然后调用drawTexCube()函数绘制贴有纹理图像的立方体(第88行)。如下图显示了接下来的这第5~10步。
drawSolidCube()函数绘制单色立方体:首先调用gl.useProgram()并将着色器程序solidProgram作为参数传入,即告诉WebGL使用这个程序。然后,调用initAttributeVariable()函数将顶点的坐标、法线分配给相应的attribute变量(第167~168行)。接着,将索引缓冲区对象绑定到gl.ELEMENT_ARRAY_BUFFER上,一切就准备好了。最后,调用gl.drawElements()函数,完成绘制操作。
drawTexCube()函数与drawSolidCube()函数的流程基本一致。额外的步骤是将纹理坐标的缓冲区分配给attribute变量(第178行),以及将纹理对象绑定到0号纹理单元上(第180~181行)。实际的绘制操作仍是由gl.drawElements()完成的,和drawSolidCube()函数一样。
一旦掌握了切换着色器的基本方法,你就可以在任意多个着色器程序中进行切换,在同一个场景中绘制出各种不同的效果的组合。