# 多个简单模型组成的复杂模型
# 层次结构模型
我们使用多个三维立方体实现一些小的模型组成,并增加动画。比如下图:
我们绘制两个立方体部件组成的机器人手臂,手臂的两个部件为 arm1 与 arm2,arm1 接在 arm2 的上面,可以将 arm1 想象成上臂,而把 arm2 想象成前臂,而肩关节在最下面。
# 实例代码
运行程序,可以通过键盘左右方向控制 arm1(同时带动整条手臂)水平转动,使用上下方向控制 arm2 绕 joint1 关节垂直转动。
// 顶点着色器
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)
}
# 着色器和着色器程序对象
创建和初始化着色器需要以下几个步骤:
- 创建着色器对象:gl.createShader();
- 向着色器对象中填充着色器程序的源代码:gl.shaderSource();
- 编译着色器:gl.compileShader();
- 创建程序对象:gl.createProgram();
- 为程序对象分配着色器: gl.attachShader();
- 连接程序对象:gl.linkProgram();
- 使用程序对象:gl.useProgram();
着色器对象:着色器对象管理一个顶点着色器或一个片元着色器。每一个着色器都有一个着色器对象。
程序对象:程序对象是管理着色器对象的容器。webgl 中,一个程序对象必须包含一个顶点着色器和一个片元着色器。
# 创建着色器对象 (gl.createShader(type))
这个函数使用如下:
对应 gl.deleteShader(shader) 用来删除着色器,参数为待删除的着色器对象。如果已经使用 gl.attachShader() 函数绑定到了程序对象上,那么并不会立刻删除着色器,而是等到程序对象不再使用该着色器后,才将其删除。
# 指定着色器对象的代码 (gl.shanderSource(shader, source))
函数使用如下
# 编译着色器 (gl.compileShader(shader))
向着色器对象传入源代码之后,需要进行编译成二进制的可执行格式才能够使用。注意,如果你通过调用 gl.shaderSource() 用新的代码替换掉了着色器中旧的代码, webgl 系统中的用旧的代码编译出的可执行部分不会被自动替换,你需要手动地重新进行编译。
在编译期间,可以使用 gl.getShaderParameter() 函数来检查着色器的状态。
该函数参数如下:
如果编译失败,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) 函数,他接收一个参数为指定程序对象。
程序对象进行着色器连接操作,目的是保证:
- 顶点着色器和片元着色器的 varying 变量同名同类型,且一一对应;
- 顶点着色器对每个 varying 变量赋了值;
- 顶点着色器和片元着色器中的同名 uniform 变量也是同类型的,无需一一对应,即某些 uniform 变量可以出现在一个着色器中而不出现在另一个中。
- 着色器中的 attribute 变量、uniform 变量和 varying 变量的个数没有超过着色器的上限。
在着色器连接之后,可以通过调用 gl.getProgramParameter(program, pname) 来检查是否连接成功,该函数使用如下:
如果程序已经成功连接,我们得到了一个二进制的可执行模块供 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
}