《WebGL入门指南》——第2章,第2.4节一个真实的3D示例

本节书摘来自异步社区《WebGL入门指南》一书中的第2章,第2.4节一个真实的3D示例,作者 【美】Tony Parisi,更多章节内容可以访问云栖社区“异步社区”公众号查看

2.4 一个真实的3D示例
WebGL入门指南
到目前为止,你也许在想“真是个还不错的正方形”,然后开始怀疑我们什么时候开始画一些真正的3D图形。好吧,那就现在吧!在示例2-2中我们将会用更有趣的物体来代替正方形,我们将会完成一个看起来还不错、并且展示了大部分WebGL主要特性、同时还保持代码简洁的页面。

图2-2就是页面的最终效果。其中我们设置了标题文字,添加了一个表面贴有图片的立方体,然后在页面底部也添加了文字。另外值得一提的是,这个页面是可以交互的:点击画布元素,立方体就会开始或停止旋转。


《WebGL入门指南》——第2章,第2.4节一个真实的3D示例

图2-2 一个进阶的Three.js示例,纹理图像来自http://www.openclipart.org(CC Public Domain Dedication授权使用)

让我们详细看一下这一切是如何完成的。下面是示例2-2的完整代码。这比我们的第一个Three.js示例要复杂一些,但是依然足够简洁,我们可以快速地浏览一下整个代码。

示例2-2 欢迎来到WebGL!

<!DOCTYPE html>
<html>
<head>
<title>Welcome to WebGL</title>
     <link rel="stylesheet" href="../css/webglbook.css" /> 
     <script src="../libs/Three.js"></script>
     <script src="../libs/RequestAnimationFrame.js"></script>
     <script>

     var renderer = null, 
          scene = null, 
          camera = null,
          cube = null,
          animating = false;

     function onLoad()
     {
        // 抓取作为Canvas容器的<div>标签
        var container = document.getElementById("container");
        // 创建Three.js渲染器,并添加到<div>标签中
        renderer = new THREE.WebGLRenderer( { antialias: true } );
        renderer.setSize(container.offsetWidth, container.offsetHeight);
        container.appendChild( renderer.domElement );
        // 创建Three.js场景
        scene = new THREE.Scene();
        // 创建相机,并添加到场景中
        camera = new THREE.PerspectiveCamera( 45, 
            container.offsetWidth / container.offsetHeight, 1, 4000 );
        camera.position.set( 0, 0, 3 );
        // 创建一个平行光光源照射到物体上
        var light = new THREE.DirectionalLight( 0xffffff, 1.5);
        light.position.set(0, 0, 1);
        scene.add( light );
        // 创建一个接受光照并带有纹理映射的立方体,并添加到场景中
        // 首先,创建一个带纹理映射的立方体
        var mapUrl = "../images/molumen_small_funny_angry_monster.jpg";
        var map = THREE.ImageUtils.loadTexture(mapUrl);

        // 然后创建一个Phong材质来处理着色,并传递给纹理映射
        var material = new THREE.MeshPhongMaterial({ map: map });
        // 创建一个立方体的几何体
        var geometry = new THREE.CubeGeometry(1, 1, 1);
        // 将几何体和材质放到一个网格中
        cube = new THREE.Mesh(geometry, material);
        // 设置网格在场景中的朝向,否则我们将不会看到立方体的形状!
        cube.rotation.x = Math.PI / 5;
        cube.rotation.y = Math.PI / 5;
        // 将立方体网格添加到场景中
        scene.add( cube );
        // 添加处理鼠标事件的函数,用于控制动画的开关
        addMouseHandler();
        // 运行渲染循环
        run();
     }
     function run()
     {
          // 渲染场景
          renderer.render( scene, camera );
          // 在下一帧中旋转立方体
          if (animating)
          {
               cube.rotation.y -= 0.01;
          }
          // 在另一帧中回调
          requestAnimationFrame(run); 
     }
     function addMouseHandler()
     {
          var dom = renderer.domElement;

          dom.addEventListener( 'mouseup', onMouseUp, false);
     }

     function onMouseUp  (event)
     {
         event.preventDefault();
         animating = !animating;
     }

     </script>
</head>
<body onLoad="onLoad();" style="">
   <center><h1>Welcome to WebGL!</h1></center>
   <div id="container" 
       style="width:95%; height:80%; position:absolute;">
   </div>
   <div id="prompt" 
      style="width:95%; height:6%; bottom:0; position:absolute;">
   Click to animate the cube
   </div>
</body>
</html>

除去我稍后会详细介绍的一些设置内容和添加了一个CSS样式表来控制字体和颜色之外,这段程序的开头部分和我们之前的那个示例非常相似。我们创建了Three.js渲染器对象,并将其DOM元素作为Canvas容器的子元素加入。这一次我们给构造函数传递了一个名为antialias的参数,并将其设置为true,告诉Three.js要启用抗锯齿(antialiased)渲染。抗锯齿可以避免绘制物体边缘时产生的锯齿。(在Three.js中有很多给方法传递参数的方式。通常来说,在对象中传递构造函数参数都是使用被命名的字段,正如在本示例中这样。)然后,我们创建了一个带透视效果的相机,和之前的示例一样。但这次,相机稍微移动了一些,以便我们可以更近地观察立方体。

2.4.1 为场景着色
我们马上就可以在场景中添加立方体了。如果往前看几行,你会发现我们使用了Three.js内置的CubeGeometry对象来创建一个单位体积的立方体。但在添加立方体之前,我们还需要做其他的一些事情。首先,我们需要对场景进行着色(shading)。如果没有着色,你将无法分辨出立方体表面的边界。我们还需要创建一个纹理映射来渲染到立方体表面,稍后会详细介绍。

为了将着色效果添加到场景中,我们需要做两件事:添加一个光源和使用另一种不同的材质。在Three.js中有好几种不同的光源。在我们的示例中,将会使用平行光(directional light),这是一种从无限远的距离(光源没有特定的位置)照向指定方向的光。Three.js的语法有些奇怪,我们要做的不是设置光源的照射方向,而是要使用position属性设置光源距离原点的位置;由此可以推论出,光源的照射方向就成了从该点指向场景原点(即照射到我们的立方体上)。

我们要的第二件事就是改变使用的材质。在Three.js中,MeshBasicMaterial用于定义属性简单的材质,比如固定颜色或是透明。这种材质不会对光照做出任何响应。所以我们要用另一种类型的材质来代替它,这就是MeshPhongMaterial。这种类型的材质应用了一种相对简单、仿真度高而又性能优越的着色模型,也就是著名的“Phong着色法”(Phong shading)。(Three.js同时还支持其他更加复杂的着色模型,我们之后会讲到。)使用了Phong着色法,我们现在就可以分辨出立方体的边缘。立方体朝向光源方向的面将会更加明亮;而背对光源的面则会相对阴暗。由此,我们即可分辨出面与面之间的边界。

图片 3 也许你已经发现了,在这个章节中我所提到的都是着色,而不是我们在第1章中提到的着色器。这是因为,我们之前说过着色器其实是一小段用类C语言写成的代码,而Three.js已经为我们内置了这样的着色器。我们只要简单的设置光源和材质,Three.js就可以用内置的着色器来替我们处理剩下的脏活累活。感谢Three.js的作者,感谢Mr.doob!

2.4.2 添加纹理映射
纹理映射(texture map),也被称为纹理(texture),是一种将位图覆盖到3D网格表面的技术。使用纹理映射可以通过简单的方式定义网格表面的颜色,同时也可以通过纹理图片的组合应用创造出更加复杂的效果,如凹凸效果和高光效果。WebGL提供了一些API调用来处理纹理,同时因为安全原因,限制了诸如跨域纹理的使用(详细信息参见第7章)。幸运的是,在Three.js中我们只要使用简单的API就可以载入纹理并与材质绑定在一起,而不需要处理太多的琐事。我们调用THREE.ImageUtils.loadTexture()方法来从一个图片文件载入纹理,然后通过向材质的构造函数传递map参数,把处理后的纹理与材质相关联。

在这里,Three.js替我们完成了很多的底层工作。它会把JPEG图像正确的映射到立方体的各个面上,并保证图像不会拉伸覆盖所有面,同时在各个面上的图像不会出现翻转或者倒立的错误。也许看起来这并不是什么大事情,但是如果你亲自用WebGL原生API来完成这件事的话,你会发现需要处理的细节太多了。而Three.js再一次替我们完成了这些困难的工作,它用内置的Phong着色器结合光源的设置、材质颜色和纹理映射的像素值,使得最终每个像素上都显示正确的颜色,并形成最后的图像效果。在Three.js中,我们可以使用纹理映射做很多事情。我们将会在随后的章节中详细讨论更多细节。

现在可以开始创建我们的立方体网格了。我们构造了一个几何体、材质和纹理,然后把它们都放到一个Three.js的网格中,并存储在变量cube中。示例2-3列出了用于创建带光照效果、纹理和Phong着色的立方体的代码。

示例2-3 创建带光照效果、纹理和Phong着色的立方体

// 创建照射物体的平行光光源
var light = new THREE.DirectionalLight( 0xffffff, 1.5);
light.position.set(0, 0, 1);
scene.add( light );
// 创建一个带着色效果和纹理映射的立方体,并添加到场景中
// 首先,创建纹理映射
var mapUrl = "../images/molumen_small_funny_angry_monster.jpg";
var map = THREE.ImageUtils.loadTexture(mapUrl);
// 然后创建一个Phong着色材质,并与纹理关联
var material = new THREE.MeshPhongMaterial({ map: map });
// 创建一个立方体的几何体
var geometry = new THREE.CubeGeometry(1, 1, 1);
// 将几何体和材质放入同一个网格中
cube = new THREE.Mesh(geometry, material);

2.4.3 旋转物体
图片 3在我们能看到立方体之前,还需要做一件事,那就是稍微旋转一下它。否则我们永远也不会发现这是一个立方体,因为它会端正地用一个面朝向我们,看起来就和我们在之前的示例中画的那个正方形一模一样,只是多了纹理贴图。所以让我们把它朝相机方向绕着x轴(水平方向)翻转一些。我们是通过设置网格的rotation属性来完成的。在Three.js中,每个物体都有位置(position)、旋转(rotation)和缩放(scale)属性。(还记得吗?我们在之前的示例中,把相机往后移动了一些。)通过给rotation.x赋一个非零值,我们告诉Three.js去为物体绕着x轴做当量的旋转。同理我们也以此处理y轴,让立方体稍微向左旋转一下。这样一来,我们就可以看到立方体6个面中的3个了。

在为物体的旋转变量赋值时,我们需要注意,大部分的3D图形系统都使用了弧度制(radians)来度量角度。弧度是指单位圆上相应角度的圆弧长度(例如,弧度制的2π就是角度制的360°)。Math.PI相当于180°,因此当我们赋值mesh.rotation.x = Math.PI / 12的时候,实际上是绕着x轴旋转了15°。

2.4.4 循环重绘和requestAnimationFrame()
你也许已经发现这个示例与之前的那个在结构上有些不同。首先,我们增加了一些辅助性的函数。其次,我们定义了一些全局变量来存储那些将用于辅助性函数的值。(好吧,我知道,这样滥用全局变量又是一个不好的习惯。但就像我承诺过的一样,在下一章我们将会应用框架结构。)同时我们还增加了一个循环函数,这就是所谓的循环重绘。通过循环重绘,我们不再只渲染一次场景,而是持续不断地渲染。这对于静态场景并不重要,但是对于含有运动物体或响应用户输入而发生变化的场景来说,我们需要持续不断地渲染场景。从现在开始,我们以后所有的示例都将使用循环重绘来渲染场景。

有很多方法来应用循环重绘。一种方法就是使用setTimeout()回调,每当场景渲染完毕后就重置超时时差。这也是一种经典的Web开发技巧,用于制作动态效果;然而它的确已经过时了,因为新的浏览器都支持一种更好的方法,那就是requestAnimationFrame()。这个函数被专门设计用于制作页面动画,当然也包括WebGL中的动画。

使用requestAnimationFrame(),浏览器可以极大地优化动画的性能表现。因为它会综合考虑所有的绘制请求,把它们都放到同一个重绘步骤中。尤其在多标签浏览器中,当动画页面处于后台时,浏览器将停止重绘以节省资源提高性能。不过并不是所有版本的所有浏览器都支持这个函数;更加添乱的是,每个浏览器中这个函数的函数名都不一样。因此,我引入了一个漂亮的工具:由Paul Irish编写的RequestAnimation Frame.js。这个文件会掩盖不同浏览器的差异性,开发者只要简单地使用requestAnimationFrame()即可。

我们已经准备好要开始渲染场景了。我们定义了一个函数run(),负责进行循环重绘。和之前一样,我们调用了渲染器的render()方法,将场景和相机传递给它。然后,我们写了一小段逻辑代码让立方体可以动起来。我们将会在下一节详细介绍。最后我们让浏览器继续渲染下一帧。参见示例2-4。

示例2-4 循环重绘

function run()
{
// 渲染场景
renderer.render( scene, camera );
// 为下一帧旋转立方体
if (animating)
{
    cube.rotation.y -= 0.01;
}
// 请求下一帧
requestAnimationFrame(run);
}

你可以在图2-2中看到最后的运行效果。现在你可以看到立方体的前侧和顶部。我们终于在页面上显示了一个真正的3D物体!

2.4.5 让页面贴近生活
作为第一个完整的示例,我们原本可以到此就结束了。我们已经在页面上绘制了一个漂亮的图形,而且是真正的3D图形。但是在今天,3D图形不只是和渲染有关了,它还需要动画和交互性。如果没有动画和交互,开发者只需要让做3D美术建模的朋友在Max和Maya中渲染好之后,存为图片然后用 标签嵌入网页就好,何必还折腾什么WebGL呢?这不是我们想要的。因此在这种思想的指导下,即使是一个简单的示例,我们也要为它加入动画和交互。

在之前的小节中,我们讨论了循环重绘。这就是我们在渲染下一帧之前改变场景的机会。为了旋转这个立方体,我们需要在每一帧都改变它的旋转属性值。我们不想让它随机乱转,而是一个绕着y轴平滑的旋转,所以我们需要做的就是每过一段时间就给它的旋转的y值做适度的增量。在WebGL中,这是一种相对简单的制作动画效果的思路。当然,还有其他的方法,但都更加复杂。我们会在以后的章节中介绍。

最后,如果我们可以控制立方体的旋转那就再好不过了。我们已经添加了一个处理鼠标点击的函数,直接使用了DOM的事件方法。其中有一个需要指出的小技巧就是,在这个示例中我们使用的DOM元素是和Three.js中渲染器对象关联的。具体的代码参考示例2-5。

示例2-5 添加鼠标交互

function addMouseHandler()
{
    var dom = renderer.domElement;
    dom.addEventListener( 'mouseup', onMouseUp, false);
}
function onMouseUp    (event)
{
    event.preventDefault();
    animating = !animating;
}

点击立方体吧!看着它旋转!是不是很沉浸其中呢?

上一篇:Git 使用基础指南


下一篇:Git多分支切换及合并代码