WebGL 切换着色器

目录

前言

如何实现切换着色器

[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. 绘制纹理立方体)

示例程序(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行)。前两者用来绘制单色的立方体,而后两者绘制贴有纹理的立方体。由于本文的重点是如何切换着色器程序对象,所以着色器的具体内容被省略了。

示例效果

示例代码

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

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

相关推荐
前端Hardy17 小时前
HTML&CSS:数据卡片可以这样设计
前端·javascript·css·3d·html
小彭努力中20 小时前
138. CSS3DRenderer渲染HTML标签
前端·深度学习·3d·webgl·three.js
小春熙子1 天前
Unity图形学之着色器之间传递参数
unity·游戏引擎·技术美术·着色器
优雅永不过时·1 天前
three.js实现地球 外部扫描的着色器
前端·javascript·webgl·three.js·着色器
AI生成未来1 天前
斯坦福&UC伯克利开源突破性视觉场景生成与编辑技术,精准描绘3D/4D世界!
3d·3d场景·4d
汪洪墩2 天前
【Mars3d】实现这个地图能靠左,不居中的样式效果
前端·javascript·vue.js·3d·webgl·cesium
Bearnaise2 天前
GaussianDreamer: Fast Generation from Text to 3D Gaussians——点云论文阅读(11)
论文阅读·人工智能·python·深度学习·opencv·计算机视觉·3d
智驾机器人技术前线2 天前
近期两篇NeRF/3DGS-based SLAM方案赏析:TS-SLAM and MBA-SLAM
3d·slam·nerf·3dgs
Tianwen_Burning2 天前
halcon3d disparity_image_to_xyz非常重要的算子及使用条件
算法·3d
allenjiao2 天前
webgl threejs 云渲染(服务器渲染、后端渲染)解决方案
webgl·云渲染·threejs·服务器渲染·后端渲染·云流化·三维云渲染