目录
前言
如何实现切换着色器
1. 准备用来绘制单色立方体的着色器
2. 准备用来绘制纹理立方体的着色器
3. 调用createProgram()函数,利用第1步创建出的着色器,创建着色器程序对象
4. 调用createProgram()函数,利用第2步创建出的着色器,创建着色器程序对象
5. 调用gl.useProgram()函数,指定使用第3步创建出的着色器程序对象。
6. 通过缓冲区对象向着色器中传入attribute变量并开启之。
7. 绘制单色立方体
8. 调用gl.useProgram()函数,指定使用第4步创建出的着色器程序对象。
9. 通过缓冲区对象向着色器传入attribute变量并开启之。
10. 绘制纹理立方体
示例程序(ProgramObject.js)
示例效果
示例代码
代码详解
前言
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行)。前两者用来绘制单色的立方体,而后两者绘制贴有纹理的立方体。由于本文的重点是如何切换着色器程序对象,所以着色器的具体内容被省略了。
示例效果
示例代码
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------v3var 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 front1.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 right1.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 down1.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 front1.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 right0.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 left0.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 down0.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 front0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, // v0-v3-v4-v5 right1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 0.0, 0.0, // v0-v5-v6-v1 up1.0, 1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, // v1-v6-v7-v2 left0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, // v7-v4-v3-v2 down0.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, // front4, 5, 6, 4, 6, 7, // right8, 9, 10, 8, 10, 11, // up12, 13, 14, 12, 14, 15, // left16, 17, 18, 16, 18, 19, // down20, 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 -> viewProjMatrixg_mvpMatrix.multiply(g_modelMatrix); // viewProjMatrix * g_modelMatrixgl.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()函数一样。
一旦掌握了切换着色器的基本方法,你就可以在任意多个着色器程序中进行切换,在同一个场景中绘制出各种不同的效果的组合。