第5课 动画
本节课要介绍的内容是:
- 使用requestAnimationFrame()方法实现动画;
- 创建FPS计数器。
程序截图如下:
其实就是把Direct3D 10教程5:3D变换的代码移植到了WebGL平台。
打开新窗口观看程序Lesson5.html。
为了使模型在场景中移动,需要重复绘制场景,而且在两次绘制场景间隔,需要更新动画模型的位置。
setInterval()和setTimeout()的使用
传统上,基于脚本的动画一直以来都是用setTimeout()和setlnterval()这两个JavaScript方法之一创建的,Canvas2D用的就是这种方法,可参见21 创建动画循环。这两个方法用来定义回调函数,我们希望在未来规定的时间(单位为毫秒)内调用这些回调函数。在回调函数中更新动画,然后再次绘制。重复地执行此过程就可以生成一个动画。这两个方法函数原型如下:
setTimeout(codeToCall,timeoutInMilliseconds) setInterval(codeToCall, timeoutlnMillisecond)
setTimeout()方法在规定的时间后(单位为毫秒,由它的第二个参数确定)调用第一个参数表示的代码或函数。这个方法有一个返回值,如果把这个返回值作为参数传给clearInterval()函数,则可取消回调函数或代码的调用。
setInterval()方法与setTimeout()方法相似,但是它每隔一定的时间(由第二个参数定义)反复调用第一个参数中的代码或函数。此外,setInterval()方法也返回一个值,如果把这个值作为参数送给clearInterval()函数,则可以取消回调函数或代码的执行。
例如下面这个例子,展示了一个非常简单的绘图循环结构,它建立在setInterval()之上:
fuction draw(){ // 绘制代码 } function startup(){ setInterval(draw,16.7); }
startup()通常是在载文档后调用的(或者在我们希望启洞动画的任何时刻调用)。在该函数中调用setInterval(),并规定在一定的时间间隔内(本例中的16.7毫秒)调用draw()函数。在这个示例中之所以选择16.7毫秒是因为通常LCD显示器的刷新频率不会大于60Hz,根据这个值就可以得到帧时间的近似值,即1/60Hz=0.0167秒=16.7毫秒。
使用requestAnimationFrame()函数
requestAnimationFrame()函数是专门为创建脚本式动画而设计的。有了一个专用的动画创建方法,浏览器供应商可以努力优化这个方法,使它比传统的setInterval()和setTimeout()方法更适合于创建动画。
用setlnterval()或setTimeout()方法创建动画时,需要确定动画更新的最佳频率。但是这个最佳频率对于动画的的设计人员来说是很难确定的。
但是,浏览器是比较容易确定这个最佳频率的。浏览器上可能会同时运行多个动画,这可能会影响这个帧频。在这种情形下,浏览器会降低所有动画的帧频,这样它们就以流畅但稍低的频率执行动画。
浏览器也可以减速更新或暂停更新在某个不可见选项卡中正在运行的动画。这样,用这个方法不仅可以改善动画的性能,还可以节省电能,后者对于移动设备尤为重要,电池寿命通常是移动设备的一个重要因素。
但requestAnimationFrame()方法的实现会有一个问题:因为不同的浏览器使用的这个函数名称稍有不同。一个实际的解决方法是使用JavaScript实用工具库webgl-utils.js中的一个小程序,这个实用工具库最早由Google公司的开发人员编写的。在webgl-utils.js中,requestAnimationFrame()的支持跨平台的小程序采用如下格式:
/** * Provides requestAnimationFrame in a cross browser way. */ window.requestAnimFrame = (function() { return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function(/* function FrameRequestCallback */ callback, /* DOMElement Element */ element) { window.setTimeout(callback, 1000/60); }; })();
可以看出,如果没有找到其他方法,则使用setTimeout()回退函数。这个实用工具库命名该方法为requestAnimFrame()而非requestAnimationFrame(),其原因是,此标准只是W3C的工作草案,还需要进一步修订。
具体代码的实现
这节课的代码以第4课为基础,不同的地方在于draw()方法中需要添加调用requestAnimFrame()函数的代码:
function draw() { requestAnimFrame(draw); var currentTime = Date.now(); elapsedTime = currentTime - previousTime; previousTime = currentTime; totalTime += elapsedTime / 1000; // 以下代码省略 }
requestAnimFrame()方法很容易使用。只需要把绘制回调函数作为它的第一个参数传入,就可以调用这个回调函数。即:
requestAnimFrame(draw);
还设置了三个全局变量:
var previousTime; // 上一帧的开始时刻 var elapsedTime = 0; // 每帧流逝的时间(毫秒) var totalTime = 0; // 动画开始起的总时间
这些时间变量在动画循环中是很有用的。比方说下draw()方法的下面的代码中就使用了totalTime作为矩阵变换的参数。代码如下:
// 绘制第一个立方体,在原地绕Y轴旋转 pushModelViewMatrix(); mat4.rotateY(modelViewMatrix, -totalTime); uploadModelViewMatrixToShader(); drawCube(); popModelViewMatrix(); // 绘制第二个立方体,缩小为原来大小的0.3倍,自转为绕Z轴旋转, // 公转为绕第一个立方体旋转 pushModelViewMatrix(); mat4.rotateY(modelViewMatrix, totalTime * 2); mat4.translate(modelViewMatrix, [2.0, 0.0, 0.0]); mat4.rotateZ(modelViewMatrix, totalTime); mat4.scale(modelViewMatrix, [0.3, 0.3, 0.3]); uploadModelViewMatrixToShader(); drawCube(); popModelViewMatrix(); }
前面我已提到,这个示例是移植自Direct3D 10教程5:3D变换,如果你看一下那篇文章中的矩阵变换,会发现有所不同:
第一个立方体绕Y轴旋转的值,在DirectX程序中是正的,在WebGL程序中是负的,这是因为DirectX是左手坐标系,OpenGL是右手坐标系,正负相反才会得到同样的效果:绕Y轴逆时针旋转。
第二个立方体的矩阵变换较复杂,在DirectX中顺序是:mScale、mSpin、mTranslate、mOrbit,而在WebGL中恰相反:mOrbit、mTranslate、mSpin、mScale,这是因为OpenGL的矩阵默认是列主序,变换矩阵是左乘的,而DirectX的矩阵是行主序的,变换矩阵是右乘的。
创建FPS计数器
定义每秒绘制多少帧(Frames Per Second,FPS)可以衡量动画的性能或动画的平稳程度。较高的FPS意味着较好的性能,即一般所谓的动画比较平稳。
理论上,FPS值比较容易计算。找们只需要测量绘制每帧所需要的时间。例如,在一帧的开始时检查时间并保存下求。在下一帧中,回到代码中的同一个地方,再次检查时间。第二次时间与第一次时间之差就是帧时间,其实就是上面代码中的elapsedTime变量,而FPS值是帧时间的倒数。
每更新一帧就计算FPS值的缺点是它可能变化太频繁。通常是计数每秒内绘制的帧,当一秒时间过去后,则把显示的帧数作为FPS值并显示。然后重新开始计数帧,又经过一秒后,再显示这个计数值,以此类推。
具体代码在draw()中:
var previousSecondTimeStamp; // 上一秒的开始时刻 var FPS = 0; // 帧数 function draw() { requestAnimFrame(draw); var currentTime = Date.now(); // 经过1秒就重新更新一次FPS if (currentTime - previousSecondTimeStamp >= 1000) { fpsCounter.innerHTML = FPS; FPS = 0; previousSecondTimeStamp = currentTime; } // 以下代码省略 … // 更新FPS FPS++; }
在1秒钟内,利用fpsCounter的innerHTML属性把FPS的值设置为在过去一秒钟时间内绘制的帧数。
你还需要在startup()函数中初始化计算FPS所需要的量个变量。另外,在HTML代码中添加一个无素,它有一个唯一的id。通过它来显示FPS值,即利用documentgetElementByld()函数得到这个的引用,然后利用innerHTML属性设置FPS值,如下所示。
<script> … function startup() { … previousTime = previousSecondTimeStamp = Date.now(); FPS = 0; … } </script> </head> <body onload="startup();"> <canvas id="lesson5-canvas" width="640" height="480"></canvas> <div id="fps-counter"> FPS: <span id="fps">--</span></div> </body>
完整js代码
<script src="glMatrix-0.9.6.js"></script> <script src="webgl-utils.js"></script> <script id="shader-vs" type="x-shader/x-vertex"> attribute vec3 aVertexPosition; attribute vec4 aVertexColor; uniform mat4 uMVMatrix; uniform mat4 uPMatrix; varying vec4 vColor; void main(void) { gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0); vColor = aVertexColor; } </script> <script id="shader-fs" type="x-shader/x-fragment"> precision mediump float; varying vec4 vColor; void main(void) { gl_FragColor = vColor; } </script> <script type="text/javascript"> var gl; var canvas; var fpsCounter; var shaderProgram; var modelViewMatrix; var projectionMatrix; var modelViewMatrixStack; function createGLContext(canvas) { var names = ["webgl", "experimental-webgl"]; var context = null; for (var i = 0; i < names.length; i++) { try { context = canvas.getContext(names[i]); } catch (e) { } if (context) { break; } } if (context) { context.viewportWidth = canvas.width; context.viewportHeight = canvas.height; } else { alert("无法创建WebGL上下文!"); } return context; } function loadShaderFromDOM(id) { var shaderScript = document.getElementById(id); // 若未找到指定id的元素则退出 if (!shaderScript) { return null; } // 遍历DOM元素的子节点,将shader代码重新构建为一个字符串 var shaderSource = ""; var currentChild = shaderScript.firstChild; while (currentChild) { if (currentChild.nodeType == 3) { // 3对应TEXT_NODE shaderSource += currentChild.textContent; } currentChild = currentChild.nextSibling; } var shader; if (shaderScript.type == "x-shader/x-fragment") { shader = gl.createShader(gl.FRAGMENT_SHADER); } else if (shaderScript.type == "x-shader/x-vertex") { shader = gl.createShader(gl.VERTEX_SHADER); } else { return null; } gl.shaderSource(shader, shaderSource); gl.compileShader(shader); if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { alert(gl.getShaderInfoLog(shader)); return null; } return shader; } function setupShaders() { vertexShader = loadShaderFromDOM("shader-vs"); fragmentShader = loadShaderFromDOM("shader-fs"); shaderProgram = gl.createProgram(); gl.attachShader(shaderProgram, vertexShader); gl.attachShader(shaderProgram, fragmentShader); gl.linkProgram(shaderProgram); if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) { alert("创建shader失败"); } gl.useProgram(shaderProgram); shaderProgram.vertexPositionAttribute = gl.getAttribLocation(shaderProgram, "aVertexPosition"); shaderProgram.vertexColorAttribute = gl.getAttribLocation(shaderProgram, "aVertexColor"); // 激活顶点着色器中的位置和颜色属性 gl.enableVertexAttribArray(shaderProgram.vertexPositionAttribute); gl.enableVertexAttribArray(shaderProgram.vertexColorAttribute); shaderProgram.pMatrixUniform = gl.getUniformLocation(shaderProgram, "uPMatrix"); shaderProgram.mvMatrixUniform = gl.getUniformLocation(shaderProgram, "uMVMatrix"); modelViewMatrix = mat4.create(); projectionMatrix = mat4.create(); modelViewMatrixStack = []; } function uploadModelViewMatrixToShader() { gl.uniformMatrix4fv(shaderProgram.mvMatrixUniform, false, modelViewMatrix); } function uploadProjectionMatrixToShader() { gl.uniformMatrix4fv(shaderProgram.pMatrixUniform, false, projectionMatrix); } function pushModelViewMatrix() { var copyToPush = mat4.create(modelViewMatrix); modelViewMatrixStack.push(copyToPush); } function popModelViewMatrix() { if (modelViewMatrixStack.length == 0) { throw "popModelViewMatrix()出错 - 栈为空"; } modelViewMatrix = modelViewMatrixStack.pop(); } var cubeVertexBuffer; var cubeVertexColorBuffer; var cubeVertexIndexBuffer; function setupBuffer() { cubeVertexBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexBuffer); var vertices = [ // 前方平面四个顶点 -0.5, -0.5, 0.5, 0, 0, 255, 255, //v0 -0.5, 0.5, 0.5, 0, 255, 0, 255, //v1 0.5, -0.5, 0.5, 0, 255, 255, 255, //v2 0.5, 0.5, 0.5, 255, 0, 0, 255, //v3 // 后方平面四个顶点 -0.5, -0.5, -0.5, 255, 0, 255, 255, //v4 -0.5, 0.5, -0.5, 255, 255, 0.0, 255, //v5 0.5, -0.5, -0.5, 255, 255, 255, 255, //v6 0.5, 0.5, -0.5, 0, 0, 0, 255 //v7 ]; // 顶点数量为8 var numberOfVertices = 8; // 计算一个顶点元素的字节数量,(x,y,z)位置为12个字节,(r,g,b,a)颜色为4个字节,此处为16 var vertexSizeInBytes = 3 * Float32Array.BYTES_PER_ELEMENT + 4 * Uint8Array.BYTES_PER_ELEMENT; // 计算一个顶点元素的浮点数数量,此处为4 var vertexSizeInFloats = vertexSizeInBytes / Float32Array.BYTES_PER_ELEMENT; // 根据上面算出的大小创建一个数组缓冲,此处这个大小为128个字节 var buffer = new ArrayBuffer(numberOfVertices * vertexSizeInBytes); // 新建一个Float32Array视图,用于访问位置数据 var positionView = new Float32Array(buffer); // 新建一个Uint8Array视图, 用于访问颜色数据 var colorView = new Uint8Array(buffer); // 将JavaScript数组的数据填充到ArrayBuffer中 var positionOffsetInFloats = 0; var colorOffsetInBytes = 12; var k = 0; // JavaScript数组中的索引 for (var i = 0; i < numberOfVertices; i++) { positionView[positionOffsetInFloats] = vertices[k]; // x positionView[1 + positionOffsetInFloats] = vertices[k + 1]; // y positionView[2 + positionOffsetInFloats] = vertices[k + 2]; // z colorView[colorOffsetInBytes] = vertices[k + 3]; // R colorView[1 + colorOffsetInBytes] = vertices[k + 4]; // G colorView[2 + colorOffsetInBytes] = vertices[k + 5]; // B colorView[3 + colorOffsetInBytes] = vertices[k + 6]; // A positionOffsetInFloats += vertexSizeInFloats; colorOffsetInBytes += vertexSizeInBytes; k += 7; } gl.bufferData(gl.ARRAY_BUFFER, buffer, gl.STATIC_DRAW); cubeVertexBuffer.positionSize = 3; cubeVertexBuffer.colorSize = 4; cubeVertexBuffer.numberOfItems = 8; cubeVertexIndexBuffer = gl.createBuffer(); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cubeVertexIndexBuffer); var cubeVertexIndices = [ 0, 3, 1, 0, 2, 3, // 前方平面 4, 5, 6, 5, 7, 6, // 后方平面 0, 1, 5, 0, 5, 4, // 左侧平面 3, 2, 6, 7, 3, 6, // 右侧平面 5, 1, 3, 5, 3, 7, // 上方平面 4, 6, 2, 4, 2, 0 // 下方平面 ]; gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(cubeVertexIndices), gl.STATIC_DRAW); cubeVertexIndexBuffer.itemSize = 1; cubeVertexIndexBuffer.numItems = 36; } var previousSecondTimeStamp; // 上一秒的开始时刻 var previousTime; // 上一帧的开始时刻 var elapsedTime = 0; // 每帧流逝的时间(毫秒) var totalTime = 0; // 动画开始起的总时间 var FPS; // 帧数 function draw() { requestAnimFrame(draw); var currentTime = Date.now(); // 经过1秒就重新更新一次FPS if (currentTime - previousSecondTimeStamp >= 1000) { fpsCounter.innerHTML = FPS; FPS = 0; previousSecondTimeStamp = currentTime; } // 每帧流逝的时间(毫秒) elapsedTime = currentTime - previousTime; previousTime = currentTime; gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); totalTime += elapsedTime / 1000; // 绘制第一个立方体,在原地绕Y轴旋转 pushModelViewMatrix(); mat4.rotateY(modelViewMatrix, -totalTime); uploadModelViewMatrixToShader(); drawCube(); popModelViewMatrix(); // 绘制第二个立方体,缩小为原来大小的0.3倍,自转为绕Z轴旋转, // 公转为绕第一个立方体旋转 pushModelViewMatrix(); mat4.rotateY(modelViewMatrix, totalTime * 2); mat4.translate(modelViewMatrix, [2.0, 0.0, 0.0]); mat4.rotateZ(modelViewMatrix, totalTime); mat4.scale(modelViewMatrix, [0.3, 0.3, 0.3]); uploadModelViewMatrixToShader(); drawCube(); popModelViewMatrix(); // 更新FPS FPS++; } function drawCube() { // 绑定同时包含顶点位置和颜色信息的缓冲 gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexBuffer); // 描述顶点位置在数组中的组织形式 gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, cubeVertexBuffer.positionSize, gl.FLOAT, false, 16, 0); // 描述顶点颜色在数组中的组织形式 gl.vertexAttribPointer(shaderProgram.vertexColorAttribute, cubeVertexBuffer.colorSize, gl.UNSIGNED_BYTE, true, 16, 12); // 绑定索引缓冲 gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cubeVertexIndexBuffer); gl.drawElements(gl.TRIANGLES, cubeVertexIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0); } function startup() { canvas = document.getElementById("lesson5-canvas"); fpsCounter = document.getElementById("fps"); gl = createGLContext(canvas); setupBuffer(); setupShaders(); previousTime = previousSecondTimeStamp = Date.now(); FPS = 0; mat4.perspective(45, gl.viewportWidth / gl.viewportHeight, 0.1, 100.0, projectionMatrix); mat4.identity(modelViewMatrix); mat4.lookAt([0, 1, 5], [0, 1, 0], [0, 1, 0], modelViewMatrix); uploadModelViewMatrixToShader(); uploadProjectionMatrixToShader(); gl.clearColor(0.0, 0.125, 0.3, 1.0); gl.enable(gl.DEPTH_TEST); draw(); } </script> </head> <body onload="startup();"> <canvas id="lesson5-canvas" width="640" height="480"></canvas> <div id="fps-counter"> FPS: <span id="fps">--</span></div> </body>文件下载(已下载 2569 次)
发布时间:2013/10/9 下午9:46:49 阅读次数:7442