WebGL 切换着色器

目录

前言 

如何实现切换着色器 

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就使用了两个着色器绘制了两个立方体,一个是纯色的,另一个贴有纹理。下图显示了程序的运行效果。

WebGL 切换着色器_第1张图片

该程序也可以帮你复习一下如何在物体表面贴上纹理。 

如何实现切换着色器 

 为了切换着色器,需要先创建多个着色器程序对象,然后在进行绘制前选择使用的程序对象。我们使用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行)。前两者用来绘制单色的立方体,而后两者绘制贴有纹理的立方体。由于本文的重点是如何切换着色器程序对象,所以着色器的具体内容被省略了。

示例效果

WebGL 切换着色器_第2张图片

示例代码

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;
}

代码详解 

WebGL 切换着色器_第3张图片

main()函数首先调用gl.createProgram()创建了两个着色器程序对象(第54、55行),该函数接收的参数和initShaders()一样,即字符串形式的顶点着色器和片元着色器代码,返回值就是着色器程序对象。两个着色器程序对象分别命名为solidProgram和texProgram。然后,获取每个着色器中各attribute变量的存储地址,保存在相应着色器程序对象的同名属性上。我们又一次用到了JavaScript的“可以随意向对象添加属性”的特性。

WebGL 切换着色器_第4张图片

接着,将顶点的数据存储在由initVertexBuffers()函数创建的缓冲区对象中。对单色立方体而言,顶点的数据包括(1)顶点的坐标,(2)法线,(3)索引。对贴有纹理的立方体而言,还得加上纹理坐标。这些缓冲区对象将在绘制立方体和切换着色器时分配给着色器中的attribute变量。

具体的,initVertexBuffers()函数首先定义了顶点坐标数组(第102行)、法线数组(第110行)、纹理坐标数组(第118行)和顶点索引数组(第126行),然后定义了一个空的Object类型的对象o,将创建的各个缓冲区对象全部添加为o的属性(第137~141行),最后返回o对象。你也通过向全局变量赋值的方式来传出缓冲区对象,但是全局变量就太多了,程序的可读性也会降低。利用函数返回对象的属性来返回多个缓冲区对象,可以帮助我们更好地管理这些缓冲区对象。 

我们使用initArrayBufferForLaterUse()函数来创建单个缓冲区对象(第137~139行),它将数组中的数据写入缓冲区,但不将缓冲区分配给attribute变量。

WebGL 切换着色器_第5张图片

回到main()函数,我们接着调用initTextures()函数建立好纹理图像(第72行),然后一切就准备好了,只等绘制两个立方体对象。首先调用drawSolidCube()函数绘制单色的立方体(第86行),然后调用drawTexCube()函数绘制贴有纹理图像的立方体(第88行)。如下图显示了接下来的这第5~10步。

WebGL 切换着色器_第6张图片

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()函数一样。

一旦掌握了切换着色器的基本方法,你就可以在任意多个着色器程序中进行切换,在同一个场景中绘制出各种不同的效果的组合。

你可能感兴趣的:(WebGL,webgl,着色器,3d,混合现实,图形渲染)