# 多个简单模型组成的复杂模型

# 层次结构模型

我们使用多个三维立方体实现一些小的模型组成,并增加动画。比如下图:

avatar

我们绘制两个立方体部件组成的机器人手臂,手臂的两个部件为 arm1 与 arm2,arm1 接在 arm2 的上面,可以将 arm1 想象成上臂,而把 arm2 想象成前臂,而肩关节在最下面。

avatar

# 实例代码

运行程序,可以通过键盘左右方向控制 arm1(同时带动整条手臂)水平转动,使用上下方向控制 arm2 绕 joint1 关节垂直转动。

avatar

// 顶点着色器
const VSHADER_SOURCE = `
  attribute vec4 a_Position;
  attribute vec4 a_Normal;
  uniform mat4 u_MvpMatrix;
  uniform mat4 u_NormalMatrix;
  varying vec4 v_Color;
  void main() {
    gl_Position = u_MvpMatrix * a_Position;
    vec3 lightDirection = normalize(vec3(0.0, 0.5, 0.7));
    vec4 color = vec4(1.0, 0.4, 0.0, 1.0);
    vec3 normal = normalize((u_NormalMatrix * a_Normal).xyz);
    float nDotL = max(dot(normal, lightDirection), 0.0);
    v_Color = vec4(color.rgb * nDotL + vec3(0.1), color.a);
  }
`

// 片元着色器
const FSHADER_SOURCE = `
  #ifdef GL_ES
  precision mediump float;
  #endif
  varying vec4 v_Color;
  void main() {
    gl_FragColor = v_Color;
  }
`
function main() {
  const canvas = document.getElementById('webgl')
  const gl = canvas.getContext('webgl')
  initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)

  const n = initVertexBuffers(gl)

  gl.clearColor(0.0, 0.0, 0.0, 1.0)
  gl.enable(gl.DEPTH_TEST)

  const u_MvpMatrix = gl.getUniformLocation(gl.program, 'u_MvpMatrix')
  const u_NormalMatrix = gl.getUniformLocation(gl.program, 'u_NormalMatrix')

  const viewProjMatrix = new Matrix4()
  viewProjMatrix.setPerspective(50.0, canvas.width / canvas.height, 1.0, 100.0)
  viewProjMatrix.lookAt(20.0, 10.0, 30.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0)

  document.onkeydown = (ev) => {
    keydown(ev, gl, n, viewProjMatrix, u_MvpMatrix, u_NormalMatrix)
  }
  draw(gl, n, viewProjMatrix, u_MvpMatrix, u_NormalMatrix)
}
function loadShader(gl, type, source) {
  const shader = gl.createShader(type)
  if (shader == null) {
    console.log('unable to create shader')
    return null
  }
  gl.shaderSource(shader, source)
  gl.compileShader(shader)
  return shader
}
function createProgam(gl, vshader, fshader) {
  const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vshader)
  const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fshader)
  if (!vertexShader || !fragmentShader) {
    return null
  }
  const program = gl.createProgram()
  if (!program) {
    return null
  }

  gl.attachShader(program, vertexShader)
  gl.attachShader(program, fragmentShader)

  gl.linkProgram(program)
  return program
}

function initShaders (gl, vshader, fshader) {
  const program = createProgam(gl, vshader, fshader)
  if (!program) {
    return false
  }
  gl.useProgram(program)
  gl.program = program
  return true
}

let ANGLE_STEP = 3.0
let g_arm1Angle = -90.0
let g_joint1Angle = 0.0

function keydown(ev, gl, n, viewProjMatrix, u_MvpMatrix, u_NormalMatrix) {
  switch (ev.keyCode) {
    case 38:
      if (g_joint1Angle < 135.0) g_joint1Angle += ANGLE_STEP
      break;
    case 40:
      if (g_joint1Angle > -135.0) g_joint1Angle -= ANGLE_STEP
      break;
    case 39:
      g_arm1Angle = (g_arm1Angle + ANGLE_STEP) % 360
      break;
    case 37:
      g_arm1Angle = (g_arm1Angle - ANGLE_STEP) % 360
      break;
    default: return
  }
  draw(gl, n, viewProjMatrix, u_MvpMatrix, u_NormalMatrix)
}

function initArrayBuffer(gl, attribute, data, type, num) {
  const buffer = gl.createBuffer()
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
  gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW)
  const a_attribute = gl.getAttribLocation(gl.program, attribute)
  gl.vertexAttribPointer(a_attribute, num, type, false, 0, 0)
  gl.enableVertexAttribArray(a_attribute)
  return true
}

function initVertexBuffers(gl) {
  // 顶点坐标(长方体的宽度为3.0,高度为10.0,长度为3.0,其原点位于其底部的中心)
  const vertices = new Float32Array([
    1.5, 10.0, 1.5, -1.5, 10.0, 1.5, -1.5,  0.0, 1.5,  1.5,  0.0, 1.5, // v0-v1-v2-v3 front
    1.5, 10.0, 1.5,  1.5,  0.0, 1.5,  1.5,  0.0,-1.5,  1.5, 10.0,-1.5, // v0-v3-v4-v5 right
    1.5, 10.0, 1.5,  1.5, 10.0,-1.5, -1.5, 10.0,-1.5, -1.5, 10.0, 1.5, // v0-v5-v6-v1 up
   -1.5, 10.0, 1.5, -1.5, 10.0,-1.5, -1.5,  0.0,-1.5, -1.5,  0.0, 1.5, // v1-v6-v7-v2 left
   -1.5,  0.0,-1.5,  1.5,  0.0,-1.5,  1.5,  0.0, 1.5, -1.5,  0.0, 1.5, // v7-v4-v3-v2 down
    1.5,  0.0,-1.5, -1.5,  0.0,-1.5, -1.5, 10.0,-1.5,  1.5, 10.0,-1.5  // v4-v7-v6-v5 back
  ])

  // 法向量
  const 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
  ])
  // 顶点索引
  const indeces = 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
  ])
  if (!initArrayBuffer(gl, 'a_Position', vertices, gl.FLOAT, 3)) return -1
  if (!initArrayBuffer(gl, 'a_Normal', normals, gl.FLOAT, 3)) return -1

  gl.bindBuffer(gl.ARRAY_BUFFER, null)
  const indexBuffer = gl.createBuffer()
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer)
  gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indeces, gl.STATIC_DRAW)
  return indeces.length
}

const g_modelMatrix = new Matrix4()
const g_mvpMatrix = new Matrix4()

function draw(gl, n, viewProjMatrix, u_MvpMatrix, u_NormalMatrix) {
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
 
  // arm1
  const arm1Length = 10.0
  g_modelMatrix.setTranslate(0.0, -12.0, 0.0)
  g_modelMatrix.rotate(g_arm1Angle, 0.0, 1.0, 0.0)
  drawBox(gl, n, viewProjMatrix, u_MvpMatrix, u_NormalMatrix)

  // arm2
  g_modelMatrix.translate(0.0, arm1Length, 0.0)
  g_modelMatrix.rotate(g_joint1Angle, 0.0, 0.0, 1.0)
  g_modelMatrix.scale(1.3, 1.0, 1.3)
  drawBox(gl, n, viewProjMatrix, u_MvpMatrix, u_NormalMatrix)
}

const g_normalMatrix = new Matrix4()

function drawBox(gl, n, viewProjMatrix, u_MvpMatrix, u_NormalMatrix) {
  g_mvpMatrix.set(viewProjMatrix)
  g_mvpMatrix.multiply(g_modelMatrix)
  gl.uniformMatrix4fv(u_MvpMatrix, false, g_mvpMatrix.elements)

  g_normalMatrix.setInverseOf(g_modelMatrix)
  g_normalMatrix.transpose()
  gl.uniformMatrix4fv(u_NormalMatrix, false, g_normalMatrix.elements)
  // 绘制
  gl.drawElements(gl.TRIANGLES, n, gl.UNSIGNED_BYTE, 0)
}

# 着色器和着色器程序对象

创建和初始化着色器需要以下几个步骤:

  1. 创建着色器对象:gl.createShader();
  2. 向着色器对象中填充着色器程序的源代码:gl.shaderSource();
  3. 编译着色器:gl.compileShader();
  4. 创建程序对象:gl.createProgram();
  5. 为程序对象分配着色器: gl.attachShader();
  6. 连接程序对象:gl.linkProgram();
  7. 使用程序对象:gl.useProgram();

着色器对象:着色器对象管理一个顶点着色器或一个片元着色器。每一个着色器都有一个着色器对象。

程序对象:程序对象是管理着色器对象的容器。webgl 中,一个程序对象必须包含一个顶点着色器和一个片元着色器。

# 创建着色器对象 (gl.createShader(type))

这个函数使用如下:

avatar

对应 gl.deleteShader(shader) 用来删除着色器,参数为待删除的着色器对象。如果已经使用 gl.attachShader() 函数绑定到了程序对象上,那么并不会立刻删除着色器,而是等到程序对象不再使用该着色器后,才将其删除。

# 指定着色器对象的代码 (gl.shanderSource(shader, source))

函数使用如下

avatar

# 编译着色器 (gl.compileShader(shader))

向着色器对象传入源代码之后,需要进行编译成二进制的可执行格式才能够使用。注意,如果你通过调用 gl.shaderSource() 用新的代码替换掉了着色器中旧的代码, webgl 系统中的用旧的代码编译出的可执行部分不会被自动替换,你需要手动地重新进行编译。

在编译期间,可以使用 gl.getShaderParameter() 函数来检查着色器的状态。

该函数参数如下:

avatar

如果编译失败,gl.getShaderParameter() 会返回 false,webgl 系统会把编译错误具体内容分写入着色器的信息日志,可以通过 gl.getShaderInforLog() 来获取,这个方法接收一个参数,参数为需要获取 shader 指定的着色器的信息日志。

# 创建程序对象 (gl.createProgram())

使用 gl.createProgram() 方法来创建程序对象。类似的,可以使用 gl.deleteProgram() 函数来删除程序对象。注意,如果该程序对象正在使用,则不立即删除,而是等他不在使用后再删除。

# 为程序对象分配着色器对象 (gl.attachShader(program, shader))

一旦程序对象被创建之后,需要向程序附上两个着色器。gl.attachShader(program, shader) 这个函数接收两个参数,第一个参数为指定程序对象,第二个参数为指定着色器对象。着色器在绑定到程序对象前,并不一定要为其指定代码进行编译,也就是说,把空的着色器绑定到程序对象也是可以的。类似的,可以使用 gl.detachShader() 函数来解除分类给程序对象的着色器。gl.detachShader(program, shader) 接收的参数跟绑定的方法一样。

# 连接程序对象 (gl.linkProgram())

在为程序对象分配了两个着色器对象后,还需要将顶点着色器和片元着色器连接起来,使用 gl.linkProgram(program) 函数,他接收一个参数为指定程序对象。

程序对象进行着色器连接操作,目的是保证:

  1. 顶点着色器和片元着色器的 varying 变量同名同类型,且一一对应;
  2. 顶点着色器对每个 varying 变量赋了值;
  3. 顶点着色器和片元着色器中的同名 uniform 变量也是同类型的,无需一一对应,即某些 uniform 变量可以出现在一个着色器中而不出现在另一个中。
  4. 着色器中的 attribute 变量、uniform 变量和 varying 变量的个数没有超过着色器的上限。

在着色器连接之后,可以通过调用 gl.getProgramParameter(program, pname) 来检查是否连接成功,该函数使用如下:

avatar

如果程序已经成功连接,我们得到了一个二进制的可执行模块供 webgl 系统使用。如果连接失败了,也可以通过调用 gl.getProgramInfiLog(program) 从信息日志中获取连接出错信息。

# 告知 webgl 系统所使用的程序对象 (gl.useProgram())

最后通过 gl.useProgram(program) 告知 webgl 系统绘制时使用哪个程序对象,该函数接收一个指定待使用的程序对象做为参数。

我们可以在绘制前初始化多个程序对象,然后在绘制的时候根据需要切换程序对象。

# 初始化着色器代码

/**
 * 初始化着色器
 * @param {*} gl webgl 上下文
 * @param {*} vshader 顶点着色器字符串代码
 * @param {*} fshader 片元着色器字符串代码
 */
function initShaders(gl, vshader, fshader) {
  // 创建程序对象
  const program = createProgram(gl, vshader, fshader)
  if (!program) {
    console.log('Failed to create Program')
    return false
  }

  gl.useProgram(program)
  gl.program = program
  return true
}
/**
 * 创建程序对象
 * @param {*} gl webgl 上下文
 * @param {*} vshader 顶点着色器字符串代码
 * @param {*} fshader 片元着色器字符串代码
 */
function createProgram(gl, vshader, fshader) {
  const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vshader)
  const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fshader)

  if (!vertexShader || !fragmentShader) {
    return null
  }

  // 创建程序对象
  const program = gl.createProgram()
  if (!program) {
    return null
  }

  // 为程序对象分配着色器对象
  gl.attachShader(program, vertexShader)
  gl.attachShader(program, fragmentShader)

  // 连接着色器 —— 这里的连接目的是将顶点着色器和片元着色器进行连接。保证顶点着色器和片元着色器的 varying 变量同名同类型,且一一对应
  // 并且顶点着色器对每个 varying 变量赋了值,顶点着色器和片元着色器中的同名 uniform 变量也是同类型的。着色器中的 attribute 变量、uniform 变量
  // 和 varying 变量的个数没有超过着色器的上限等。
  gl.linkProgram(program)

  // 检查连接状态
  const linked = gl.getProgramParameter(program, gl.LINK_STATUS)

  if (!linked) {
    const error = gl.getProgramInfoLog(program)
    console.log(`Failed to link program: ${error}`)
    gl.deleteProgram(program)
    gl.deleteShader(fragmentShader)
    gl.deleteShader(vertexShader)
    return null
  }
  return program
}

/**
 * 
 * @param {*} gl webgl 上下文
 * @param {*} type 创建着色器对象的类型
 * @param {*} source 着色器对应的字符串代码
 */
function loadShader(gl, type, source) {
  const shader = gl.createShader(type)
  if (shader === null) {
    console.log('unable to create shader')
    return null
  }

  // 指定着色器对象的代码
  gl.shaderSource(shader, source)
  // 编译着色器
  gl.compileShader(shader)

  const compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS)

  if (!compiled) {
    const error = gl.getShaderInfoLog(shader)
    console.log(`Failed to compile shader: ${error}`)
    gl.deleteShader(shader)
    return null
  }

  return shader
}

评 论:

更新: 11/23/2020, 1:40:16 AM