- 学习使用一个矩阵变换库,该库封装了矩阵运算的数学细节。
- 快速上手使用该矩阵库,对图形进行复合变换。
- 在该矩阵库的帮助下,实现简单的动画效果。
矩阵变换库:cuon-matrix.js
OpenGL中的函数:
书中 cuon-matrix.js 函数库中是这样实现的:
函数库主要创建了一个Matrix4
类(构造函数),在该类原型函数下绑定了众多方法,许多函数之前都留有注释,我们主要看一下代码最前面的注释:
/**
* This is a class treating 4x4 matrix.
* This class contains the function that is equivalent to OpenGL matrix stack.
* The matrix after conversion is calculated by multiplying a conversion matrix from the right.
* The matrix is replaced by the calculated result.
*/
根据注释可知,该函数库主要处理4×4的矩阵,对标OpenGL中矩阵处理函数。函数库提供了一个名为Matrix4
的对象(构造函数),我们可以通过new方法创建它的实例,对象内部挂载了许多关于矩阵计算的方法。
这样使用函数库的方法,写起来更加简单
- 矩阵的创建方式:
// RotatedTriangle_Matrix.js
...
// 创建旋转矩阵
let radian = (Math.PI * ANGLE) / 180.0 // 转换为弧度制
let cosB = Math.cos(radian)
let sinB = Math.sin(radian)
// 注意WebGL中矩阵是列主序的
let xformMatrix = new Float32Array([
cosB, sinB, 0.0, 0.0,
-sinB, cosB, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0,
])
...
// RotatedTriangle_Matrix4.js
...
// 创建旋转矩阵
// 为旋转矩阵创建Matrix4对象
let xformMatrix = new Matrix4()
// 将xformMatrix设置为旋转矩阵
xformMatrix.setRotate(ANGLE, 0, 0, 1)
...
- 传输矩阵数据
// RotatedTriangle_Matrix.js
...
// 将旋转图形所需数据传输给顶点着色器
let u_xformMatrix = gl.getUniformLocation(gl.program, 'u_xformMatrix')
if (!u_xformMatrix) {
console.log('Failed to get the storage location of u_xformMatrix')
}
gl.uniformMatrix4fv(u_xformMatrix, false, xformMatrix)
...
// RotatedTriangle_Matrix4.js
...
let u_xformMatrix = gl.getUniformLocation(gl.program, 'u_xformMatrix')
if (!u_xformMatrix) {
console.log('Failed to get the storage location of u_xformMatrix')
}
gl.uniformMatrix4fv(u_xformMatrix, false, xformMatrix.elements)
...
Matrix4在不提供数据直接初始化的情况下,其挂载的element元素就是一个单位阵,之后大部分操作都是对element元素进行的。
从上表可见,Matrix4对象有两种方法:
- 一种方法的名称中含有前缀set,这一类方法会根据参数计算出变换矩阵,然后将变换矩阵写入自身;
- 另一种方法的名称中不含set前缀,这一类方法在计算出变换矩阵后,会将自身与计算出的变换矩阵相乘,最终结果写入Matrix4对象。
复合变换RotatedTranslatedTriangle.js
相关内容:采用Matirx4对象,运用矩阵乘法模拟符合变换。
相关函数:Matrix4.setRotate(), Matrix4.translate()注意:矩阵乘法的先后顺序与模型变换的先后顺序有关,不能随意变化(矩阵乘法不满足乘法交换律)。
注意是 左乘
一个模型可能经过了多次变换,将这些变换全部复合成一个等效的变换,就得到了模型变换(model transformation),或称建模变换(modeling transformation),相应的,模型变换的矩阵称为模型矩阵(model matrix)。
重写Rotatedtriangle_Matrix4 代码:
- 命名习惯,在顶点着色器中命名uniform变量(mat4)为u_ModelMatrix,取模型矩阵之意。
// 顶点着色器
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' +
'uniform mat4 u_ModelMatrix;\n' +
'void main(){\n' +
' gl_Position = u_ModelMatrix * a_Position;\n' +
'}\n'
- 构建矩阵时,采用Matrix4对象,先设置对象为旋转矩阵,再和平移矩阵相乘。
// 创建Matrix4对象
let modelMatrix = new Matrix4()
// 旋转角度
let ANGLE = 90.0
// 平移距离
let Tx = 0.5
// 设置模型矩阵为旋转矩阵
modelMatrix.setRotate(ANGLE, 0, 0, 1)
// 将模型矩阵乘以平移矩阵
modelMatrix.translate(Tx, 0, 0)
// 将模型矩阵传输给顶点着色器
let u_ModelMatrix = gl.getUniformLocation(gl.program, 'u_ModelMatrix')
if (!u_ModelMatrix) {
console.log('Failed to get the storage location of u_ModelMatrix')
}
gl.uniformMatrix4fv(u_ModelMatrix, false, modelMatrix.elements)
完整代码:
// RotatedTranslatedTriangle.js
// 顶点着色器
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' +
'uniform mat4 u_ModelMatrix;\n' +
'void main(){\n' +
' gl_Position = u_ModelMatrix * a_Position;\n' +
'}\n'
// 片元着色器
var FSHADER_SOURCE =
'void main(){\n' + ' gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n' + '}\n'
// 主函数
function main() {
// 获取canvas元素
let canvas = document.getElementById('webgl')
// 获取WebGL上下文
let gl = getWebGLContext(canvas)
if (!gl) {
console.log('Failed to get the rendering context for WebGL')
return
}
// 初始化着色器
if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
console.log('Failed to initialize shaders')
return
}
// 设置顶点位置
let n = initVertexBuffers(gl)
if (n < 0) {
console.log('Failed to set the positions of the vertices')
return
}
// 创建Matrix4对象
let modelMatrix = new Matrix4()
// 旋转角度
let ANGLE = 90.0
// 平移距离
let Tx = 0.5
// 设置模型矩阵为旋转矩阵
modelMatrix.setRotate(ANGLE, 0, 0, 1)
// 将模型矩阵乘以平移矩阵
modelMatrix.translate(Tx, 0, 0)
// 将模型矩阵传输给顶点着色器
let u_ModelMatrix = gl.getUniformLocation(gl.program, 'u_ModelMatrix')
if (!u_ModelMatrix) {
console.log('Failed to get the storage location of u_ModelMatrix')
}
gl.uniformMatrix4fv(u_ModelMatrix, false, modelMatrix.elements)
// 设置背景色
gl.clearColor(0.0, 0.0, 0.0, 1.0)
// 清空绘图区
gl.clear(gl.COLOR_BUFFER_BIT)
// 绘制三角形
gl.drawArrays(gl.TRIANGLES, 0, n)
}
function initVertexBuffers(gl) {
// 设置类型化数组和顶点数
let vertices = new Float32Array([0.0, 0.3, -0.3, -0.3, 0.3, -0.3])
let n = 3
// 创建缓冲区对象
let vertexBuffer = gl.createBuffer()
if (!vertexBuffer) {
console.log('Failed to create the buffer object')
return -1
}
// 绑定缓冲区
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)
// 缓冲区写入数据
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STREAM_DRAW)
let a_Position = gl.getAttribLocation(gl.program, 'a_Position')
if (a_Position < 0) {
console.log('Failed to get the storage location of a_Position')
return -1
}
// 将缓冲区分配给attribute变量
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0)
// 开启attribute变量(连接)
gl.enableVertexAttribArray(a_Position)
return n
}
动画-RotatingTriangle.js
相关内容:通过JavaScript灵活设计WebGL系统,通过反复变换和重绘图形生成动画效果;setInterval()系列函数和requestAnimationgFrame()系列函数的异同。
相关函数:setInterval(), requestAnimationFrame(), cancelAnimationFrame()
机制一:在t0、t1、t2、t3等时刻反复调用同一个函数来绘制三角形。
机制二:每次绘制之前,清除上次绘制的内容,并使三角形旋转相应的角度。
基于此,该实例程序与前面的示例有以下三点区别:
- 实现反复调用绘制函数的机制(机制一)
- 定义绘制函数,在绘制函数中包括清空绘图区、向着色器传值、绘制三步
- 由于程序需要反复绘制,所以在一开始就指定了背景色。
// RotatedTranslatedTriangle.js
// 顶点着色器
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' +
'uniform mat4 u_ModelMatrix;\n' +
'void main(){\n' +
' gl_Position = u_ModelMatrix * a_Position;\n' +
'}\n'
// 片元着色器
var FSHADER_SOURCE =
'void main(){\n' + ' gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n' + '}\n'
// 旋转速度(度/秒)
var ANGLE_STEP = 45.0
// 主函数
function main() {
// 获取canvas元素
let canvas = document.getElementById('webgl')
// 获取WebGL上下文
let gl = getWebGLContext(canvas)
if (!gl) {
console.log('Failed to get the rendering context for WebGL')
return
}
// 初始化着色器
if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
console.log('Failed to initialize shaders')
return
}
// 设置顶点位置
let n = initVertexBuffers(gl)
if (n < 0) {
console.log('Failed to set the positions of the vertices')
return
}
// 设置背景色
gl.clearColor(0.0, 0.0, 0.0, 1.0)
// 获取u_ModelMatrix变量存储位置
let u_ModelMatrix = gl.getUniformLocation(gl.program, 'u_ModelMatrix')
if (!u_ModelMatrix) {
console.log('Failed to get the storage location of u_ModelMatrix')
}
// 三角形当前旋转角度
let currentAngle = 0.0
// 模型矩阵,Matrix4对象
let modelMatrix = new Matrix4()
// 开始绘制三角形
let tick = function () {
currentAngle = animate(currentAngle) // 更新旋转角
draw(gl, n, currentAngle, modelMatrix, u_ModelMatrix)
requestAnimationFrame(tick) // 请求浏览器调用tick
}
tick()
}
function initVertexBuffers(gl) {
// 设置类型化数组和顶点数
let vertices = new Float32Array([0.0, 0.3, -0.3, -0.3, 0.3, -0.3])
let n = 3
// 创建缓冲区对象
let vertexBuffer = gl.createBuffer()
if (!vertexBuffer) {
console.log('Failed to create the buffer object')
return -1
}
// 绑定缓冲区
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)
// 缓冲区写入数据
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STREAM_DRAW)
let a_Position = gl.getAttribLocation(gl.program, 'a_Position')
if (a_Position < 0) {
console.log('Failed to get the storage location of a_Position')
return -1
}
// 将缓冲区分配给attribute变量
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0)
// 开启attribute变量(连接)
gl.enableVertexAttribArray(a_Position)
return n
}
function draw(gl, n, currentAngle, modelMatrix, u_ModelMatrix) {
// 设置旋转矩阵
modelMatrix.setRotate(currentAngle, 0, 0, 1)
// 旋转前加个平移
// modelMatrix.translate(0.5, 0, 0)
// 将旋转矩阵传输给顶点着色器
gl.uniformMatrix4fv(u_ModelMatrix, false, modelMatrix.elements)
// 清空绘图区
gl.clear(gl.COLOR_BUFFER_BIT)
// 绘制三角形
gl.drawArrays(gl.TRIANGLES, 0, n)
}
// 记录上一次调用函数的时刻
var g_last = Date.now()
// 更新旋转角
function animate(angle) {
// 计算距离上次调用经过多长时间
let now = Date.now()
let elapsed = now - g_last // 毫秒
g_last = now
// 根据上次调用的时间,更新当前旋转角度
let newAngle = angle + (ANGLE_STEP * elapsed) / 1000.0
return (newAngle %= 360)
}
重复调用函数的方法:
// 开始绘制三角形
let tick = function () {
currentAngle = animate(currentAngle) // 更新旋转角
draw(gl, n, currentAngle, modelMatrix, u_ModelMatrix) // 绘制三角形
requestAnimationFrame(tick) // 请求浏览器调用tick
}
tick()
该方法是一个诞生较早的方法,其出现时浏览器还没有支持多标签页,所以在现代浏览器中,不论标签页是否被激活,该方法都会反复调用func,如果标签页较多,就会增加浏览器的负荷。
这一方法只有当标签页处于激活状态时才会生效,基于此也无法指定重复调用的时间间隔。
更新旋转角度:
...
// 旋转速度(度/秒)
var ANGLE_STEP = 45.0
...
// 记录上一次调用函数的时刻
var g_last = Date.now()
// 更新旋转角
function animate(angle) {
// 计算距离上次调用经过多长时间
let now = Date.now()
let elapsed = now - g_last // 毫秒
g_last = now
// 根据上次调用的时间,更新当前旋转角度
let newAngle = angle + (ANGLE_STEP * elapsed) / 1000.0
return (newAngle %= 360)
}
animate()是更新旋转角的主要部分,该函数配合当前旋转角currentAngle变量、全局变量ANGLE_STEP变量(旋转速度)和g_last(上一次调用函数的时刻)使用,基本思路是:根据本次调用与上次调用之间的时间间隔来决定这一帧的旋转角度比上一帧大出多少
绘制函数的设计:
/**
* 绘制三角形
* @param {WebGLRenderingContext} gl 上下文对象
* @param {number} n 顶点数量 int
* @param {number} currentAngle 当前旋转角度 float
* @param {Martix4} modelMatrix 根据当前的旋转角度计算出的旋转矩阵,存储在Martix4对象中
* @param {number} u_ModelMatrix 顶点着色器中同名的uniform变量的存储位置 uint
*/
function draw(gl, n, currentAngle, modelMatrix, u_ModelMatrix) {
// 设置旋转矩阵
modelMatrix.setRotate(currentAngle, 0, 0, 1)
// 将旋转矩阵传输给顶点着色器
gl.uniformMatrix4fv(u_ModelMatrix, false, modelMatrix.elements)
// 清空绘图区
gl.clear(gl.COLOR_BUFFER_BIT)
// 绘制三角形
gl.drawArrays(gl.TRIANGLES, 0, n)
}
参考:【《WebGL编程指南》读书笔记-高级变换与动画基础】_webgl高级-CSDN博客