WebGL教程3:运动起来

欢迎来到WebGL教程第三课。这次我们将学习如何移动物体。本课基于NeHe OpenGL教程的第4课。

如果你的浏览器已经支持WebGL,请点击此处,你将看到本课WebGL的现场版;如果不支持,你从此处可以获取一个支持WebGL的浏览器。





一点提示:这些课程是面向那些具有一定编程知识但没有实际3D图形开发经验的开发人员的;其目的是让你对代码层上发生了什么事 有很好的理解,以便你能尽可 能快地创建出自己的3D网页。如果你还没看过第一课和第二课的话,你应该在开始本课之前看 看它们。因为我在这里仅仅解释与第二课代码的不同之处和一些新的代码。



同之前的课程一样,本课也可能存在一些缺陷和错误概念。如果你发现有什么不对的话,请留言让我知道,我会纠正它。



获取这个例子的代码有两种方法:一种就是当你观看实时版的时候点击“查看源码”的链接,另一种是你从 GitHub的代码库获取(包括 以后课程的代码)。对于任一种方式,一旦你获得源码,你就可以用你喜欢的文本编辑器打开并查看它。



在讲解代码之前,我要澄清一件事。在webGL中制作3D场景的动画是十分容易的——你只需重复地绘制该场景,每次都把它绘制 得不一样。这对许多读者来说也许是一件显而易见的事,但当我开始学习webGL时,对此却有点惊讶。可能对于那些第一次使用webGL绘制3D图形的人来 说也有点吃惊吧。起初让我困惑的原因是,我想象它应该使用更高级的抽象方法,即它应该这样运行:“告诉3D系统有一个正方形在点X处(我起初绘制它的地方),接着移动这个正方形,告诉3D系统该正方形已经移动到了点Y处。”然而事实是:“你告诉3D系统有一个正方形在点X处,接着在下次绘制它时,告诉系 统它在点Y处,再下一次它在点Z处”,以此类推。



我希望上面这段话至少能让部分人有一个更加清晰的概念(如果它让人迷惑的话,请留言给我,我将删除它:-)



由于到目前为止我们的示例代码一直使用drawScene函数来绘 制物体,并一直使用如下代码:

setInterval(drawScene, 15);



来告诉JavaScript每隔15ms就调用一次drawScene函 数,为了制作场景动画并让三角形和正方形移动,我们所需要做的就是改变此处代码以便每次调用drawScene函数时,它绘制的物体略有不同。



这意味着我们对第二课中的代码改动最大地方在drawScene函数中,因此让我们就从这里(大约在index.html文件三分之二的地方)开始吧。第一件需要注意的事就是在函数声明之前,我们要定义两个新的全局变量。

var rTri = 0;

var rSquare = 0;



这两个变量分别用来 跟踪三角形和正方形的旋转。它们都从0度开始旋转,然后角度将随时间增加——稍后你将看到如何进行——,从而渐渐旋转(提示:在一个三维程序中像这样使用 全局变量并不是很好的应用。我将在第九课中使用一种更合适的方式来构造程序。)



对drawScene函数的另一个改变在我们绘制三角形的点。我将通过上下文的方式来介绍绘制三角形的所有代码,新添加的代码用红色标示:

    perspective(45, gl.viewportWidth / gl.viewportHeight,0.1, 100.0);
    loadIdentity();

    mvTranslate([-1.5, 0.0, -7.0])

    mvPushMatrix();
    mvRotate(rTri, [0, 1, 0]);

    gl.bindBuffer(gl.ARRAY_BUFFER,triangleVertexPositionBuffer);
   gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute,triangleVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);

    gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexColorBuffer);
    gl.vertexAttribPointer(shaderProgram.vertexColorAttribute,triangleVertexColorBuffer.itemSize, gl.FLOAT, false, 0, 0);

    setMatrixUniforms();
    gl.drawArrays(gl.TRIANGLES, 0,triangleVertexPositionBuffer.numItems);

    mvPopMatrix();

为了解释这些代码,让我们回到第一课。在那里,我曾经说过:在OpenGL中,当我们绘制一个场景时,你要告诉它用“当前 的”旋转方法在“当前的”位置上绘制每一个物体——因此,例如你说“向前移动20个单位,旋转32度,接着绘制机器人”,这非常有用,因为你能将“绘制机器人”的代码封装在一个函数中,然后,只需在调用函数前改变“平移/旋转”参数,就能轻松绘制机器人。

你应该记得这个当前状态存储于一个模型视图矩阵中。考虑到这点,下面这个函数调用的目的是十分显而易见的:

mvRotate(rTri, [0, 1, 0]);



改变存储在模型视图矩阵中的当前旋转状态,围绕垂直轴(通过第二个矢量参数指定)旋转rTri度。这意味着绘制三角形时,三角形将被旋转rTri度。mvRotate函数就像我们在第一课中看到的mvTranslate函数一样使用JavaScript编写——稍后我们再来看它。



那么,mvPushMatrix和mvPopMatrix这两个函数又是做什么的呢?通过函数名,你可能会猜到他们也和模型视图矩阵有关。回到先前绘制机器人的那个例子,处在最高层的代码需要移至A点,绘制机器人,接着从A点做些偏移并绘制一个茶壶。绘制机器 人的代码可能会给模型视图矩阵带来各种各样的变化;它可能从机器人的身体开始绘制,然后向下移动到腿部,接着向上移动到头部,最后绘制完胳膊。问题是如果你在绘制完机器人之后试图移动至偏移点,那此时的移动不是相对于A点,而是相对于最后绘制的点。这就意味着如果机器人抬起了它的胳膊,那么茶壶的位置也将向上移动。这可不是什么好事情。



现在需要做的,是在你开始绘制机器人之前将模型视图矩阵的状态存储起来,之后再将其恢复。当然,这就是mvPushMatrix和mvPopMatrix这两个函数所做的事情。mvPushMatrix将矩阵放入一个堆栈,而 mvPopMatrix放弃当前矩阵并从堆栈顶部取出一个矩阵,然后恢复其状态。使用堆栈意味着我们可以嵌套任意多层的绘图代码,每层对模型视图矩阵进行操作,然后再将其恢复。因此,一旦绘制好旋转的三角形,我们应该用mvPopMatrix来恢复模型视图矩阵,所以代码如下:

mvTranslate([3.0, 0.0, 0.0]);



...在一个非旋转的参考帧中移动整个场景。(如果对此仍然不是很清楚的话,我建议你拷贝该代码并移除push/pop代码看看会发生什么,然后再重新运行它,不同的效果很快就会显现)



因此,对代码的这三处改变将使得三角形围绕垂直轴的中心旋转,但并不影响正方形。同样也有三行类似的代码使得正方形围绕水平轴的中心旋转。

    mvPushMatrix();
    mvRotate(rSquare, [1, 0, 0]);

    gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
   gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute,squareVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);

    gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexColorBuffer);
    gl.vertexAttribPointer(shaderProgram.vertexColorAttribute,squareVertexColorBuffer.itemSize, gl.FLOAT, false, 0, 0);

    setMatrixUniforms();
    gl.drawArrays(gl.TRIANGLE_STRIP, 0,squareVertexPositionBuffer.numItems);

    mvPopMatrix();
  }

... 以上就是drawScene函数所有变动的地方。



显然,为了将场景制作成动画我们还需要做另一件事,这就是随时间的变化而改变rTri和 rSquare的数值,以使每次绘制的场景略有不同。我们使用animate函数来做到这一点,它就像drawScene函数一样每隔一段时间就被调用一次。其代码如下:

var lastTime = 0;
  function animate() {
    var timeNow = new Date().getTime();
    if (lastTime != 0) {
      var elapsed = timeNow - lastTime;
      rTri += (90 * elapsed) / 1000.0;
      rSquare += (75 * elapsed) / 1000.0;
    }
    lastTime = timeNow;

  }



一种制作场景动画的简单方法是在每次调用animate时增加固定值(这也是我编写本教程所参考的的原始教程所使用的方法),但在这里我将使用一种我认为比较好的方法:用距离函数上次被调用的时间长短来决定一个物体旋转多少。特别地,三角形每秒旋转90度,正方形每秒旋转75度。这样做的好处是:无论你们的机器有多快,大家在场景中看到的都是相同的移动速度;只是在较慢的机器上图像会发生抖动。这对于像本例这样一个简单演示并不重要,但是对于像游戏或类似的应用就比较重要了。



接下来的变化是我们必须每隔一段时间有规律地调用animate,就像对drawScene所做的那样。我们创建一个名为tick的新函数,该函数用来调用 这两个函数并且自身每隔15毫秒被调用一次。

function tick() {
    drawScene();
    animate();
  }

  function webGLStart() {
    var canvas =document.getElementById("lesson03-canvas");
    initGL(canvas);
    initShaders();
    initTexture();

    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    gl.clearDepth(1.0);

    gl.enable(gl.DEPTH_TEST);
    gl.depthFunc(gl.LEQUAL);

    setInterval(tick, 15);

  }



这就是在绘制并制作场景动画代码中所有变动的地方。现在,让我们来看看需要添加的代码。首先是mvPushMatrix和mvPopMatrix:

  var mvMatrixStack = [];

  function mvPushMatrix(m) {
    if (m) {
      mvMatrixStack.push(m.dup());
      mvMatrix = m.dup();
    } else {
      mvMatrixStack.push(mvMatrix.dup());
    }
  }

  function mvPopMatrix() {
    if (mvMatrixStack.length == 0) {
      throw "Invalid popMatrix!";
    }
    mvMatrix = mvMatrixStack.pop();
    return mvMatrix;

  }



这里并没有什么令人吃惊的地方。我们用一个列表来保留矩阵堆栈并适当地定义push和pop。



现在,来看看mvRotate函数:

function mvRotate(ang, v) {
    var arad = ang * Math.PI / 180.0;
    var m = Matrix.Rotation(arad, $V([v[0], v[1],v[2]])).ensure4x4();
    multMatrix(m);

  }



创建一个矩阵用以表示旋转的所有困难工作通过Sylvester库来完成——这非常简单。

 

目 录 第1章 WebGL简介 1 1.1 WebGL——一个技术定义 2 1.2 3D图形学——入门 4 1.2.1 3D坐标系 4 1.2.2 网格、多边形和顶点 5 1.2.3 材质、纹理和光源 5 1.2.4 变换与矩阵 6 1.2.5 相机、透视、视口和投影 6 1.2.6 着色器 7 1.3 WebGL原生API 8 1.3.1 WebGL应用结构剖析 9 1.3.2 画布元素与绘制上下文 9 1.3.3 视口 10 1.3.4 Buffer、ArrayBuffer和类型化数组 10 1.3.5 矩阵 11 1.3.6 着色器 12 1.3.7 绘制图元 13 1.4 本章小结 14 第2章 你的第一个WebGL程序 15 2.1 Three.js——一个JavaScript 3D引擎 15 2.2 建立Three.js运行环境 17 2.3 一个简单的Three.js网页 17 2.4 一个真实的3D示例 20 2.4.1 为场景着色 23 2.4.2 添加纹理映射 24 2.4.3 旋转物体 25 2.4.4 循环重绘和requestAnimationFrame() 25 2.4.5 让页面贴近生活 26 2.5 本章小结 27 第3章 图形 28 3.1 Sim.js——一个轻量级的WebGL模拟框架 29 3.2 创建网格 30 3.3 使用材质、纹理和光源 34 3.3.1 光源的种类 35 3.3.2 使用多重纹理创建更具真实感的场景 37 3.3.3 纹理与透明 42 3.4 构建变换层级 42 3.5 创建自定义几何体 46 3.6 点和线的渲染 49 3.6.1 使用粒子系统绘制点 50 3.6.2 线的绘制 52 3.7 编写着色器 53 3.7.1 WebGL着色器基础 53 3.7.2 Three.js中的着色器 55 3.8 本章小结 60 第4章 动画 61 4.1 动画基础 61 4.1.1 帧动画 61 4.1.2 时间动画 62 4.1.3 插值与补间动画 62 4.1.4 关键帧 63 4.1.5 关节动画 64 4.1.6 蒙皮动画 64 4.1.7 目标变形动画 64 4.2 使用Tween.js库来创建补间动画 65 4.2.1 创建一个基本的补间动画 66 4.2.2 带缓动效果的补间动画 68 4.3 为带关节的模型制作关键帧动画 71 4.3.1 载入模型 71 4.3.2 为模型制作动画 73 4.4 材质和光源动画 76 4.5 纹理动画 78 4.6 蒙皮动画和变形动画 80 4.7 本章小结 80 第5章 交互 81 5.1 点击检测、拾取和投影 81   Three.js中的点击检测 82 5.2 处理鼠标移入和点击 85 5.3 处理拖曳 88   在拖曳中使用补间动画 91 5.4 使用点击点和法线信息 91 5.5 基于相机的交互 92 5.5.1 利用镜头控制制作一个模型浏览器 93 5.5.2 场景漫游 95 5.6 本章小结 96 第6章 2D与3D的整合 98 6.1 整合动态HTML和WebGL 99 6.1.1 创建DIV元素弹出层 99 6.1.2 利用2D屏幕坐标为3D物体添加注释 103 6.1.3 为3D场景添加背景图片 104 6.2 在2D页面上插入3D浮层 105 6.3 利用2D Canvas创建动态纹理 107 6.4 使用视频作为纹理 115 6.5 渲染动态3D文字 119 6.6 WebGL中的终极整合 121 6.7 本章小结 123 第7章 实战WebGL 124 7.1 如何选择运行库和框架 124 7.2 载入3D内容 126 7.2.1 COLLADA:数字资产交换格式 126 7.2.2 Three.js中的JSON模型文件格式 130 7.2.3 Three.js二进制模型文件格式 134 7.2.4 压缩3D模型 135 7.2.5 Three.js中的JSON场景文件格式 136 7.3 创建3D内容 137 7.3.1 从Blender中导出3D内容 137 7.3.2 把OBJ文件转换为Three.js JSON文件 139 7.3.3 把OBJ文件转换为Three.js二进制文件 139 7.3.4 其他软件或格式的转换 139 7.4 浏览器支持度 140 7.4.1 检测浏览器WebGL支持 141 7.4.2 在Safari中开启WebGL支持 142 7.5 处理丢失上下文事件 143 7.6 WebGL的安全性 146 7.7 本章小结 149 第8章 你的第一个WebGL游戏 150 8.1 构建游戏的各个部分 151 8.1.1 相机、角色和控制 152 8.1.2 美术设计 159 8.1.3 模型预览器 161 8.1.4 创建粒子系统 163 8.1.5 添加声音 166 8.2 万物归一 167 8.3 本章小结 180 后记 181 附录A WebGL在线资源 183
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值